大纲
前言
版本说明
组件 | 版本 |
---|
SpringBoot | 2.2.1.RELEASE |
Spring Security | 5.2.1.RELEASE |
用户认证
用户认证是指验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码来完成认证过程。通俗点说就是系统判断用户是否能登录。
设置用户名和密码
第一种方式
在配置文件 application.yml
中,指定用户名和密码。
1 2 3 4 5 6
| spring: security: user: password: "123456" authorities: "manager" name: "admin"
|
第二种方式
创建配置类,并指定用户名和密码。
代码下载
本章节完整的案例代码可以直接从 GitHub 下载对应章节 spring-security5-02
。
1 2 3 4 5 6 7 8 9 10 11 12
| @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encodePassword = passwordEncoder.encode("123456"); auth.inMemoryAuthentication().withUser("admin").password("{bcrypt}" + encodePassword).authorities("manager"); }
}
|
Spring Security 5 新增支持多种加密算法,在开发者没有指定 PasswordEncoder
的时候,默认采用 Bcrypt 加密算法,但还需要指定 encodingId
,否则在用户登录时会出现以下的错误,详细的错误分析过程请看 这里
1
| There is no PasswordEncoder mapped for the id "null"
|
也就是必须在经过加密之后的密文前面加上 {encodingId}
,其中的 encodingId
可以简单理解为加密算法的类型,密码加密后完整的书写格式如下:
1
| {bcrypt}$2a$10$rY/0dflGbwW6L1yt4RVA4OH8aocD7tvMHoChyKY/XtS4DXKr.JbTC
|
若使用 MD5 加密算法,那么密码加密后完整的书写格式如下:
1
| {MD5}e10adc3949ba59abbe56e057f20f883e
|
PasswordEncoderFactories.createDelegatingPasswordEncoder()
方法的底层源码如下:
最佳实践
在日常开发中,建议手动指定 PasswordEncoder
,这样就不要指定 encodingId
了,示例代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encodePassword = passwordEncoder.encode("123456"); auth.inMemoryAuthentication().passwordEncoder(passwordEncoder) .withUser("admin") .password(encodePassword) .authorities("manager"); }
}
|
或者使用 @Bean
注解注入 PasswordEncoder
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encodePassword = passwordEncoder.encode("123456"); auth.inMemoryAuthentication().withUser("admin").password(encodePassword).authorities("manager"); }
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
}
|
第三种方式
实现 UserDetailsService
接口,并指定用户名和密码。
代码下载
本章节完整的案例代码可以直接从 GitHub 下载对应章节 spring-security5-03
。
- 实现
UserDetailsService
接口,指定用户信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service;
@Service("userDetailsService") public class LoginServiceImpl implements UserDetailsService {
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { List<GrantedAuthority> authors = AuthorityUtils.commaSeparatedStringToAuthorityList("manager"); BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encodePassword = passwordEncoder.encode("123456"); return new User("admin", encodePassword, authors); }
}
|
- 创建配置类,指定
UserDetailsService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private UserDetailsService userDetailsService;
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); }
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
}
|
读取数据库的数据
这里的案例,主要演示如何基于上述介绍的第三种用户认证方式(实现 UserDetailsService
接口),从数据库中读取用户名和密码,并自定义登录页面。
代码下载
本章节完整的案例代码可以直接从 GitHub 下载对应章节 spring-security5-04
。
创建数据库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| CREATE DATABASE `spring_security_study` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `spring_security_study`;
create table user( id bigint primary key auto_increment, username varchar(20) unique not null, password varchar(100) );
insert into user values(1, 'wangwu', '$2a$10$IwvZiSm3vdhRtdyU8rJQz.pb9U/kYHorC2aQqwtFX.RVuFFHOpt82'); insert into user values(2, 'zhangsan', '$2a$10$IwvZiSm3vdhRtdyU8rJQz.pb9U/kYHorC2aQqwtFX.RVuFFHOpt82');
|
引入依赖项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.1.RELEASE</version> <relativePath/> </parent>
<properties> <mybatis-plus.version>3.0.5</mybatis-plus.version> </properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
|
创建登录页面
在项目的 /src/main/resources/static/
目录下,创建 login.html
页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Login Page</title> </head> <body> <div> <form action="/user/login" method="post"> 用户名: <input type="text" name="username"/><br/> 密码: <input type="password" name="password"/><br/> <input type="submit" value="登录"/> </form> </div> </body> </html>
|
特别注意
- 1、页面的提交方式必须为
post
请求 - 2、用户名、密码的参数名称必须为
username
、password
- 3、如果需要更改用户名和密码的参数名称,可以参考 这里 的内容
编写业务代码
1 2 3 4 5 6 7 8 9 10 11 12
| @Data @TableName(value = "user") public class User implements Serializable {
@TableId(type = IdType.AUTO) private Long id;
private String username;
private String password;
}
|
1 2 3 4
| @Mapper public interface UserMapper extends BaseMapper<User> {
}
|
- 实现
UserDetailsService
接口,从数据库中获取用户名和密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.clay.security.entity.User; import com.clay.security.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List;
@Service("userDetailsService") public class LoginServiceImpl implements UserDetailsService {
@Autowired private UserMapper userMapper;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUsername, username); User userEntity = userMapper.selectOne(queryWrapper); if (userEntity == null) { throw new UsernameNotFoundException("User name not exist"); } List<GrantedAuthority> authors = AuthorityUtils.commaSeparatedStringToAuthorityList("manager"); return new org.springframework.security.core.userdetails.User(userEntity.getUsername(), userEntity.getPassword(), authors); }
}
|
- 创建配置类,指定
UserDetailsService
和自定义的登录页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private UserDetailsService userDetailsService;
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); }
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") .loginProcessingUrl("/user/login") .defaultSuccessUrl("/hello") .and() .authorizeRequests().antMatchers("/login.html", "/user/login").permitAll() .anyRequest().authenticated() .and().csrf().disable(); }
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
}
|
1 2 3 4 5 6 7 8 9
| @RestController public class HelloController {
@RequestMapping("/hello") public String hello() { return "Hello Spring Security"; }
}
|
1 2 3 4 5 6 7 8 9
| @SpringBootApplication @MapperScan("com.clay.security.**.mapper") public class MainApplication {
public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); }
}
|
创建配置文件
这里主要配置数据源和 MyBatis Plus。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource url: jdbc:mysql://127.0.0.1:3306/spring_security_study?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2B8 username: root password: 123456
mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml typeAliasesPackage: com.clay.security.**.entity global-config: db-config: id-type: AUTO banner: false configuration: map-underscore-to-camel-case: true call-setters-on-nulls: true jdbc-type-for-null: 'null' log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
测试项目代码
浏览器访问 http://127.0.0.1:8080/hello
,然后用户名输入 wangwu
,密码输入 123456
,若能跳转到 /hello
页面,则说明应用正常运行。
用户退出登录
这里主要演示用户成功登录一段时间后,主动点击 退出登录
的链接,以此退出登录系统。
代码下载
本章节完整的案例代码可以直接从 GitHub 下载对应章节 spring-security5-08
。
退出登录案例
- 在登录成功后跳转到的页面中,添加一个退出登录的链接
1 2 3 4 5 6 7 8 9 10 11
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Login Successed</title> </head> <body> <h2>登录成功</h2><br> <a href="/logout">退出登录</a> </body> </html>
|
1 2 3 4 5 6 7 8 9 10 11
| @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override protected void configure(HttpSecurity http) throws Exception { http.logout() .logoutUrl("/logout") .logoutSuccessUrl("/").permitAll(); }
}
|
用户自动登录
这里主要演示如何基于数据库实现 自动登录 (记住我)
的功能,让用户在一段时间内可以自动登录进系统,无需输入用户名和登录密码。
代码下载
本章节完整的案例代码可以直接从 GitHub 下载对应章节 spring-security5-09
。
自动登录原理
自动登录流程
底层核心代码
自动登录案例
创建数据库
下述的 persistent_logins
表用于存储自动登录生成的 Token,该数据库表的 DDL 语句可以在 JdbcTokenRepositoryImpl
类中找到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| CREATE DATABASE `spring_security_study` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `spring_security_study`;
CREATE TABLE `persistent_logins` ( `username` varchar(64) NOT NULL, `series` varchar(64) NOT NULL, `token` varchar(64) NOT NULL, `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`series`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
引入依赖项
值得一提的是,由于 Spring Security 底层使用 JDBC 访问数据库,因此这里需要引入 JDBC 的依赖,也可以直接引入 MyBatis 或者 MyBatis-Plus 的 Starter。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.1.RELEASE</version> <relativePath/> </parent>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
|
创建登录页面
特别注意
记录密码单选框的 name
属性值必须为 remember-me
,不能改为其他值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Login Page</title> </head> <body> <div> <form action="/user/login" method="post"> 用户名: <input type="text" name="username"/><br/> 密码: <input type="password" name="password"/><br/> <input type="checkbox" name="remember-me" title="记住密码"/><span>记住密码</span><br/> <input type="submit" value="登录"/> </form> </div> </body> </html>
|
创建配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
@Configuration public class RememberMePersistentConfig {
@Autowired private DataSource dataSource;
@Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private UserDetailsService userDetailsService;
@Autowired private PersistentTokenRepository tokenRepository;
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); }
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") .loginProcessingUrl("/user/login") .defaultSuccessUrl("/hello") .and() .authorizeRequests().antMatchers("/", "/login.html", "/user/login").permitAll() .anyRequest().authenticated() .and().csrf().disable();
http.rememberMe() .tokenRepository(tokenRepository) .tokenValiditySeconds(60) .userDetailsService(userDetailsService); }
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
}
|
创建配置文件
1 2 3 4 5 6 7 8
| spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource url: jdbc:mysql://127.0.0.1:3306/spring_security_study?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2B8 username: root password: 123456
|
测试项目代码
- 勾选
记住密码
的单选框,成功登录后,观察本地浏览器是否生成了名称为 remember-me
的 Cookie
- 查看
persistent_logins
数据库表,观察是否生成了相应的记录
1 2 3 4 5 6
| mysql> select * from persistent_logins; +----------+--------------------------+--------------------------+---------------------+ | username | series | token | last_used | +----------+--------------------------+--------------------------+---------------------+ | admin | rLe+EpMfgqKEzC+bquqDZg== | QBzgbfV5VXdppdd6/wnrPw== | 2018-05-06 21:45:54 | +----------+--------------------------+--------------------------+---------------------+
|