学习源码:https://github.com/xbhou-cn/spring-security-for-learn
目录
Spring Boot 集成 Spring Security
Spring Security简介
Spring Security致力于为Java应用提供认证和授权管理。它是一个强大的,高度自定义的认证和访问控制框架。
具体介绍参见https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/
这句话包括两个关键词:Authentication(认证)和 Authorization(授权,也叫访问控制)
认证:系统认为用户是否能登陆
授权:系统判定用户是否有权限做某事
Spring Security是重量级框架,各方面功能比较全面,还有一种常用的轻量级安全框架shiro,常用的安全管理技术栈的组合是这样的:
- SSM+Shiro
- Spring Boot/Spring Cloud +Spring Security
核心模块:
Core - spring-security-core.jar
Remoting - spring-security-remoting.jar
Web - spring-security-web.jar
Config - spring-security-config.jar
LDAP - spring-security-ldap.jar
OAuth 2.0 Core - spring-security-oauth2-core.jar
OAuth 2.0 Client - spring-security-oauth2-client.jar
OAuth 2.0 JOSE - spring-security-oauth2-jose.jar
ACL - spring-security-acl.jar
CAS - spring-security-cas.jar
OpenID - spring-security-openid.jar
Test - spring-security-test.jar
Spring Boot 集成 Spring Security
第一步:引入Spring Security所需依赖
<!--Spring boot 安全框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
第二步:编写测试类
package xb.hou.modules.security.rest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @title: TestController
* @Author xbhou
* @Date: 2021-04-22 22:43
* @Version 1.0
*/
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
public String hello() {
return "Hello World";
}
}
第三步:进行测试,访问http://localhost:8000/test/hello,发现画面跳转到登陆画面,说明集成成功,需要认证之后才能访问,默认用户名是user,密码在项目启动时在控制台打印出了,认证后跳转到访问的接口
Spring security基本原理
Spring security本质上就是过滤器链
UserDetailsService接口讲解
自定义逻辑控制认证功能,继承该接口,重写查询用户的逻辑进行认证
过程:
- 继承UsernamePasswordAuthenticationFilter,重写attemptAuthentication(用户验证),successfulAuthentication(认证成功),unsuccessfulAuthentication(认证失败方法)
- 创建类实现UserDetailsService接口,编写查询数据过程,返回User对象(或者创建自定义对象继承User)
PasswordEncoder接口
对密码进行加密
@Bean
public PasswordEncoder passwordEncoder() {
// 密码加密方式
return new BCryptPasswordEncoder();
}
用户认证
- 通过配置文件
spring:
security:
user:
name: admin
password: admin
- 通过配置类
package xb.hou.modules.security.config;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @title: SecurityConfig
* @Author xbhou
* @Date: 2021-04-25 22:14
* @Version 1.0
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder encoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 重写该方法进行用户认证配置
auth.inMemoryAuthentication().withUser("admin").password(encoder.encode("admin")).roles("admin");
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 自定义编写实现实现UserDetailsService
- 第一步 创建配置类,设置使用哪一个UserDetailsService实现类
package xb.hou.modules.security.config;
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;
/**
* @title: SecurityConfig
* @Author xbhou
* @Date: 2021-04-25 22:14
* @Version 1.0
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 第二步 编写实现类, 返回User对象,User对象用用户名,密码和操作权限
package xb.hou.modules.security.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @title: UserDetailsServiceImpl
* @Author xbhou
* @Date: 2021-04-25 22:28
* @Version 1.0
*/
@Service("userService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(() -> "admin");
// 抛出UsernameNotFoundException则表示用户不存在,认证不通过
return new User("admin", passwordEncoder.encode("admin"), authorities);
}
}
- 查询数据库的方式下认证
package xb.hou.modules.security.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import xb.hou.modules.security.domain.SysUser;
import xb.hou.modules.security.repository.UserRepository;
import java.util.ArrayList;
import java.util.List;
/**
* @title: UserDetailsServiceImpl
* @Author xbhou
* @Date: 2021-04-25 22:28
* @Version 1.0
*/
@Service("userService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
// 通过用户名查询用户
SysUser user = userRepository.findByUserName(userName);
if (user == null) {
// 用户不存在抛出异常
throw new UsernameNotFoundException("用户不存在!");
}
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(() -> "admin");
// 返回数据库查询到的数据
return new User(user.getUserName(), passwordEncoder.encode(user.getPassword()), authorities);
}
}
自定义登陆画面和设置不需要认证的访问
- 在配置类实现相关配置
package xb.hou.modules.security.config;
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;
/**
* @title: SecurityConfig
* @Author xbhou
* @Date: 2021-04-25 22:14
* @Version 1.0
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义自己编写的登陆画面
.loginPage("/login.html") //登陆画面
.loginProcessingUrl("/user/login") //登陆访问路径,不用我们自己实现
.defaultSuccessUrl("/test/index").permitAll() //登陆成功 跳转路径
.and().authorizeRequests().antMatchers("/", "/test/hello", "/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 创建登陆画面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<!--请求方式必须是post,请求地址要和配置的地址相同-->
<form action="/user/login" method="post">
<!--用户名和密码输入框的name必须是username和password-->
用户名:<input type="text" name="username"/>
<br/>
密码:<input type="password" name="password"/>
<br/>
<input type="submit" value="login"/>
</form>
</body>
</html>
基于角色或权限进行访问控制
hasAuthority方法
如果当前的主体具有指定的权限,则返回true,否则返回false,一般只针对一个权限操作
-
在配置类设置当前访问地址有哪些权限
package xb.hou.modules.security.config;
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;
/**
* @title: SecurityConfig
* @Author xbhou
* @Date: 2021-04-25 22:14
* @Version 1.0
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义自己编写的登陆画面
.loginPage("/login.html") //登陆画面
.loginProcessingUrl("/user/login") //登陆访问路径,不用我们自己实现
.defaultSuccessUrl("/test/index").permitAll() //登陆成功 跳转路径
.and().authorizeRequests().antMatchers("/", "/test/hello", "/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
// 只有用户具有admin权限才能访问该路径
.antMatchers("/test/index").hasAuthority("admin")
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
-
在UserDetailsService,把返回的User对象设置权限
package xb.hou.modules.security.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import xb.hou.modules.security.domain.SysUser;
import xb.hou.modules.security.repository.UserRepository;
import java.util.ArrayList;
import java.util.List;
/**
* @title: UserDetailsServiceImpl
* @Author xbhou
* @Date: 2021-04-25 22:28
* @Version 1.0
*/
@Service("userService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
// 通过用户名查询用户
SysUser user = userRepository.findByUserName(userName);
if (user == null) {
// 用户不存在抛出异常
throw new UsernameNotFoundException("用户不存在!");
}
List<GrantedAuthority> authorities = new ArrayList<>();
// 给用户添加admin权限
authorities.add(() -> "admin");
// 返回数据库查询到的数据
return new User(user.getUserName(), passwordEncoder.encode(user.getPassword()), authorities);
}
}
hasAnyAuthority方法
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true
- 在配置类设置当前访问地址有哪些权限
package xb.hou.modules.security.config;
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;
/**
* @title: SecurityConfig
* @Author xbhou
* @Date: 2021-04-25 22:14
* @Version 1.0
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义自己编写的登陆画面
.loginPage("/login.html") //登陆画面
.loginProcessingUrl("/user/login") //登陆访问路径,不用我们自己实现
.defaultSuccessUrl("/test/index").permitAll() //登陆成功 跳转路径
.and().authorizeRequests().antMatchers("/", "/test/hello", "/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
// 用户具有admin或者test权限都能访问该路径
.antMatchers("/test/index").hasAnyAuthority("admin,test")
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 在UserDetailsService,把返回的User对象设置权限,同上
hasRole方法
- 在配置类设置当前访问地址有哪些权限
// 用户具有sale角色权限才能访问,会自动添加ROLE_
.antMatchers("/test/index").hasRole("sale")
- 在UserDetailsService,把返回的User对象设置权限,同上
// 添加sale角色
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");
hasAnyRole方法
满足其中一个配置条件,配置和实现方式同上
自定义没有访问权限画面
配置没有访问权限需要跳转的画面
@Override
protected void configure(HttpSecurity http) throws Exception {
//没有访问权限跳转的画面
http.exceptionHandling().accessDeniedPage("/unauth.html");
}
注解的使用
方法授权类型 | 声明方式 | JSR标准 | 允许SpEL表达式 |
@PreAuthorize @PostAuthorize | 注解 @EnableGlobalMethodSecurity(prePostEnabled = true) | No | Yes |
@RolesAllowed @PermitAll @DenyAll | 注解 @EnableGlobalMethodSecurity(jsr250Enabled = true) | Yes | NO |
@Secure | 注解 @EnableGlobalMethodSecurity(securedEnabled = true) | No | No |
@Secured
判断是否具有角色,另外注意的是这里匹配的字符串需要添加前缀"ROLE_"。
@Secured({"ROLE_ADMIN"})
使用注解前先要开启注解功能
@EnableGlobalMethodSecurity(securedEnabled = true)
package xb.hou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
/**
* @title: AppRun
* @Author xbhou
* @Date: 2021-04-16 17:20
* @Version 1.0
*/
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class AppRun {
public static void main(String[] args) {
SpringApplication.run(AppRun.class, args);
}
}
@PreAuthorize
注解适合进入方法前的校验,@PreAuthorize可以将登陆用户的roles/permissions参数传到方法中
@GetMapping("/hello")
@PreAuthorize("hasAnyAuthority('menu:system')")
public String hello() {
return "Hello World";
}
使用注解前先要开启注解功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PostAuthorize
注解使用并不多,在方法执行后再进行权限认证,适合验证带有返回值的权限
@GetMapping("/hello")
@PostAuthorize("hasAnyAuthority('menu:system')")
public String hello() {
return "Hello World";
}
开启方式同上
@PreFilter和@PostFilter
进入控制器前和权限验证之后对数据进行过滤,一般用到的不多
@PreFilter在执行方法之前过滤集合或数组.
@PostMapping("/hello")
@PreFilter(value = "filterObject%2==0")
public String hello(@RequestBody List<Integer> num) {
System.out.println(num);
return "Hello";
}
// 参数是 [1,2,3,4,5]
// 运行打印的结果是[2, 4]
@PostFilter执行该方法后,过滤返回的集合或数组.
@GetMapping("/hello")
@PostFilter(value = "filterObject.username=='admin'")
public List<User> hello() {
List<User> u = new ArrayList<>();
u.add(new User("admin", "password", AuthorityUtils.commaSeparatedStringToAuthorityList("")));
u.add(new User("admin1", "password1", AuthorityUtils.commaSeparatedStringToAuthorityList("")));
System.out.println(u);
return u;
}
退出操作
- 在配置类中进行配置
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置退出
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
// 配置没有权限跳转的画面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin() //自定义自己编写的登陆画面
.loginPage("/login.html") //登陆画面
.loginProcessingUrl("/user/login") //登陆访问路径,不用我们自己实现
.defaultSuccessUrl("/success.html").permitAll() //登陆成功 跳转路径
.and().authorizeRequests().antMatchers("/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
- 登陆成功后跳转到success.html,退出直接访问/logout
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆成功</title>
</head>
<body>
<a href="/logout">退出</a>
</body>
</html>
测试是访问login.html,访问/test/index的话,登陆成功后会继续访问该url,会覆盖配置的登陆成功需要访问的路径
@GetMapping("/index")
@PreAuthorize("hasAnyAuthority('menu:list')")
public String index() {
System.out.println("nihao");
return "Hello index";
}
基于数据库实现自动登陆
自动登陆的实现方式:
- cokie技术
- 基于安全框架的自动登陆
原理:
- 认证成功后,后台返回前端一段加密串,用cookie保存,后台把加密串和用户信息保存在数据库
- cookie可以设置有效时长,再次访问时,拿加密串到后台获取用户信息
流程
- 首次认证的源码解析
package org.springframework.security.web.authentication;
...
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
protected ApplicationEventPublisher eventPublisher;
protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private AuthenticationManager authenticationManager;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private RememberMeServices rememberMeServices = new NullRememberMeServices();
private RequestMatcher requiresAuthenticationRequestMatcher;
private boolean continueChainBeforeSuccessfulAuthentication = false;
private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
private boolean allowSessionCreation = true;
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
// 认证方法
...
// 认证通过后调用该方法
this.successfulAuthentication(request, response, chain, authResult);
}
}
...
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
// 认证成功,将token写入数据库
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
...
}
...
public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "remember-me";
public static final String DEFAULT_PARAMETER = "remember-me";
public static final int TWO_WEEKS_S = 1209600;
private static final String DELIMITER = ":";
protected final Log logger = LogFactory.getLog(this.getClass());
protected final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserDetailsService userDetailsService;
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private String cookieName = "remember-me";
private String cookieDomain;
private String parameter = "remember-me";
private boolean alwaysRemember;
private String key;
private int tokenValiditySeconds = 1209600;
private Boolean useSecureCookie = null;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
protected AbstractRememberMeServices(String key, UserDetailsService userDetailsService) {
Assert.hasLength(key, "key cannot be empty or null");
Assert.notNull(userDetailsService, "UserDetailsService cannot be null");
this.key = key;
this.userDetailsService = userDetailsService;
}
public final void loginFail(HttpServletRequest request, HttpServletResponse response) {
this.logger.debug("Interactive login attempt was unsuccessful.");
this.cancelCookie(request, response);
this.onLoginFail(request, response);
}
protected void onLoginFail(HttpServletRequest request, HttpServletResponse response) {
}
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
if (!this.rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
} else {
// 到达这一步,继续向下执行,到PersistentTokenBasedRememberMeServices
this.onLoginSuccess(request, response, successfulAuthentication);
}
}
protected abstract void onLoginSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3);
}
...
public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
private SecureRandom random = new SecureRandom();
public static final int DEFAULT_SERIES_LENGTH = 16;
public static final int DEFAULT_TOKEN_LENGTH = 16;
private int seriesLength = 16;
private int tokenLength = 16;
public PersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
super(key, userDetailsService);
this.tokenRepository = tokenRepository;
}
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
} else {
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
} else if (!presentedToken.equals(token.getTokenValue())) {
this.tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
} else if (token.getDate().getTime() + (long) this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'");
}
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());
try {
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
this.addCookie(newToken, request, response);
} catch (Exception var9) {
this.logger.error("Failed to update token: ", var9);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
}
return this.getUserDetailsService().loadUserByUsername(token.getUsername());
}
}
}
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
this.logger.debug("Creating new persistent login for user " + username);
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());
try {
// 通过PersistentTokenRepository创建token,存入数据库
// 因此,想要把token存入数据库,则需要实例化JdbcTokenRepositoryImpl,设置DataSource
this.tokenRepository.createNewToken(persistentToken);
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}
}
...
}
2. 实现UserDetailsService,并配置用户登陆查询方式
- 实现UserDetailsService
package xb.hou.service;
import lombok.RequiredArgsConstructor;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
final PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return new User("admin", passwordEncoder.encode("admin"), AuthorityUtils.commaSeparatedStringToAuthorityList("user:list"));
}
}
- 指定用户登陆的查询方式
package xb.hou.config;
import lombok.RequiredArgsConstructor;
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.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
/**
* @title: SecurityConfig
* @Author xbhou
* @Date: 2021-06-17 13:23
* @Version 1.0
*/
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
final private PasswordEncoder passwordEncoder;
final private UserDetailsService userDetailsService;
final private PersistentTokenRepository repository;
/**
* 重写该方法,并指定用户查询实现方式
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置退出
http.logout().logoutUrl("/logout").logoutSuccessUrl("/user/logout").permitAll();
// 没有权限跳转到unauth.html
http.exceptionHandling().accessDeniedPage("/unauth.html");
// 登陆方式
http.formLogin().loginPage("/login.html") //登陆画面
.loginProcessingUrl("/user/login") //登陆的form的action
.defaultSuccessUrl("/success.html").permitAll() // 登陆成功后跳转的画面
.and().authorizeRequests().anyRequest().authenticated() // 其他访问需要认证,以及授权
.and().rememberMe().tokenRepository(repository) // token记录方式
.tokenValiditySeconds(3600) // 过期时间,单位为秒
.and().csrf().disable();
}
}
- login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<!--请求方式必须是post,请求地址要和配置的地址相同-->
<form action="/user/login" method="post">
<!--用户名和密码输入框的name必须是username和password-->
用户名:<input type="text" name="username"/>
<br/>
密码:<input type="password" name="password"/>
<br/>
<div>
<!--自动登陆的name必须是remember-me-->
<label><input type="checkbox" name="remember-me"/>自动登录</label>
<button type="submit">立即登陆</button>
</div>
</form>
</body>
</html>
- success.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆成功</title>
</head>
<body>
<a href="/logout">退出</a>
</body>
</html>
- unauth.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>错误</title>
</head>
<body>
<h1>没有访问权限!</h1>
</body>
</html>
3. 配置数据库连接,实例化JdbcTokenRepositoryImpl,设置DataSource
- 数据库配置信息
server:
port: 8000
#配置数据源
spring:
main:
allow-bean-definition-overriding: true
#配置 Jpa
jpa:
hibernate:
ddl-auto: update
properties:
dialect: org.hibernate.dialect.MySQL5Dialect
open-in-view: true
datasource:
druid:
db-type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost/demo?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
# 初始连接数
initial-size: 5
# 最小连接数
min-idle: 15
# 最大连接数
max-active: 30
# 超时时间(以秒数为单位)
remove-abandoned-timeout: 180
# 获取连接超时时间
max-wait: 3000
# 连接有效性检测时间
time-between-eviction-runs-millis: 60000
# 连接在池中最小生存的时间
min-evictable-idle-time-millis: 300000
# 连接在池中最大生存的时间
max-evictable-idle-time-millis: 900000
# 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除
test-while-idle: true
# 指明是否在从池中取出连接前进行检验,如果检验失败, 则从池中去除连接并尝试取出另一个
test-on-borrow: true
# 是否在归还到池中前进行检验
test-on-return: false
# 检测连接是否有效
validation-query: select 1
- 实例化JdbcTokenRepositoryImpl,设置DataSource
- 使用JdbcTokenRepositoryImpl需要先建表,或者setCreateTableOnStartup为true
CREATE TABLE persistent_logins ( username VARCHAR ( 64 ) NOT NULL, series VARCHAR ( 64 ) PRIMARY KEY, token VARCHAR ( 64 ) NOT NULL, last_used TIMESTAMP NOT NULL)
package xb.hou.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* @title: BeanConfig
* @Author xbhou
* @Date: 2021-06-24 16:40
* @Version 1.0
*/
@Configuration
@RequiredArgsConstructor
public class BeanConfig {
final private DataSource dataSource;
/**
* 实例化JdbcTokenRepositoryImpl,设置DataSource
* @return
*/
@Bean
public PersistentTokenRepository getRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
/**
* 加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4. 记住密码再次登陆时,源码解析
- RememberMeAuthenticationFilter
package org.springframework.security.web.authentication.rememberme;
...
public class RememberMeAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware {
private ApplicationEventPublisher eventPublisher;
private AuthenticationSuccessHandler successHandler;
private AuthenticationManager authenticationManager;
private RememberMeServices rememberMeServices;
public RememberMeAuthenticationFilter(AuthenticationManager authenticationManager, RememberMeServices rememberMeServices) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
this.authenticationManager = authenticationManager;
this.rememberMeServices = rememberMeServices;
}
public void afterPropertiesSet() {
Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
Assert.notNull(this.rememberMeServices, "rememberMeServices must be specified");
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
// 调用rememberMeServices的autoLogin方法进行登陆,进入autoLogin方法
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
try {
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
this.onSuccessfulAuthentication(request, response, rememberMeAuth);
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
}
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
}
if (this.successHandler != null) {
this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
return;
}
} catch (AuthenticationException var8) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with remember-me token, as AuthenticationManager rejected Authentication returned by RememberMeServices: '" + rememberMeAuth + "'; invalidating remember-me token", var8);
}
this.rememberMeServices.loginFail(request, response);
this.onUnsuccessfulAuthentication(request, response, var8);
}
}
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
}
chain.doFilter(request, response);
}
}
protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
}
protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
}
public RememberMeServices getRememberMeServices() {
return this.rememberMeServices;
}
}
- AbstractRememberMeServices
package org.springframework.security.web.authentication.rememberme;
public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "remember-me";
public static final String DEFAULT_PARAMETER = "remember-me";
public static final int TWO_WEEKS_S = 1209600;
private static final String DELIMITER = ":";
protected final Log logger = LogFactory.getLog(this.getClass());
protected final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserDetailsService userDetailsService;
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private String cookieName = "remember-me";
private String cookieDomain;
private String parameter = "remember-me";
private boolean alwaysRemember;
private String key;
private int tokenValiditySeconds = 1209600;
private Boolean useSecureCookie = null;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
protected AbstractRememberMeServices(String key, UserDetailsService userDetailsService) {
Assert.hasLength(key, "key cannot be empty or null");
Assert.notNull(userDetailsService, "UserDetailsService cannot be null");
this.key = key;
this.userDetailsService = userDetailsService;
}
public void afterPropertiesSet() {
Assert.hasLength(this.key, "key cannot be empty or null");
Assert.notNull(this.userDetailsService, "A UserDetailsService is required");
}
/**
* 自动登陆方法
* @param request
* @param response
* @return
*/
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
// 获得cookie
String rememberMeCookie = this.extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
} else {
this.logger.debug("Remember-me cookie detected");
if (rememberMeCookie.length() == 0) {
this.logger.debug("Cookie was empty");
this.cancelCookie(request, response);
return null;
} else {
UserDetails user = null;
try {
// 对cookie解密
String[] cookieTokens = this.decodeCookie(rememberMeCookie);
// 通过cookie获取用户
user = this.processAutoLoginCookie(cookieTokens, request, response);
// 验证用户
this.userDetailsChecker.check(user);
this.logger.debug("Remember-me cookie accepted");
//
return this.createSuccessfulAuthentication(request, user);
} catch (CookieTheftException var6) {
this.cancelCookie(request, response);
throw var6;
} catch (UsernameNotFoundException var7) {
this.logger.debug("Remember-me login was valid but corresponding user not found.", var7);
} catch (InvalidCookieException var8) {
this.logger.debug("Invalid remember-me cookie: " + var8.getMessage());
} catch (AccountStatusException var9) {
this.logger.debug("Invalid UserDetails: " + var9.getMessage());
} catch (RememberMeAuthenticationException var10) {
this.logger.debug(var10.getMessage());
}
this.cancelCookie(request, response);
return null;
}
}
}
...
protected abstract UserDetails processAutoLoginCookie(String[] var1, HttpServletRequest var2, HttpServletResponse var3) throws RememberMeAuthenticationException, UsernameNotFoundException;
...
}
- PersistentTokenBasedRememberMeServices
package org.springframework.security.web.authentication.rememberme;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.Assert;
public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
private SecureRandom random = new SecureRandom();
public static final int DEFAULT_SERIES_LENGTH = 16;
public static final int DEFAULT_TOKEN_LENGTH = 16;
private int seriesLength = 16;
private int tokenLength = 16;
public PersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
super(key, userDetailsService);
this.tokenRepository = tokenRepository;
}
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
} else {
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
// 从数据库查询token信息
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
} else if (!presentedToken.equals(token.getTokenValue())) {
this.tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
} else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'");
}
// 每次登陆更新数据库
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());
try {
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
this.addCookie(newToken, request, response);
} catch (Exception var9) {
this.logger.error("Failed to update token: ", var9);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
}
// 通过UserDetailsService获取用户
return this.getUserDetailsService().loadUserByUsername(token.getUsername());
}
}
}
...
}