SpringSecurity 实现rember me 功能
添加该功能是在原有功能上新增功能:SpringBoot +SpringSecurity+mysql 实现用户数据权限管理
本文仅做重点代码的和相关依赖说明:SpringBoot +SpringSecurity+mysql 实现用户数据权限管理 文章中,我们采用的了分布式架构搭建该项目,导致controller 模块是不存在数据库连接资源(DataSource),由此,我们在controller 模块需要添加关于mysql 的连接和相关配置参数:
pom.xml 文件添加MySQL jar文件依赖。
<!-- spring-security 实现remember me 功能 -->
<!-- mysql数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.12</version>
</dependency>
<!-- 数据层 Spring-data-jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
application.properties 添加数据库相关配置参数:
#mysql setting
spring.datasource.url=jdbc:mysql://192.168.1.73:3306/boot-security?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=digipower
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update
登入界面(login.html)添加rember-me 的复选框。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta content="text/html;charset=UTF-8"/>
<title>登录</title>
<link rel="stylesheet" th:href="@{static/css/bootstrap.min.css}"/>
<style type="text/css">
body { padding: 20px; }
.starter-template { width:350px; padding: 0 40px; text-align: center; }
</style>
</head>
<body>
<p>
<a th:href="@{/index}"> INDEX</a>
<a th:href="@{/admin}"> | ADMIN</a>
<a th:href="@{/hello}"> | HELLO</a>
<br/>
</p>
<hr/>
<div class="starter-template">
<p th:if="${param.logout}" class="bg-warning">已成功注销</p><!-- 1 -->
<p th:if="${param.error}" class="bg-danger">有错误,请重试</p> <!-- 2 -->
<h2>使用用户名密码登录</h2>
<form name="form" th:action="@{/login}" action="/login" method="POST"> <!-- 3 -->
<div class="form-group">
<label for="username">账号</label>
<input type="text" class="form-control" name="username" value="" placeholder="账号" />
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" class="form-control" name="password" placeholder="密码" />
</div>
<div class="form-group">
<label for="remember-me">是否记住</label>
<input type="checkbox" name="remember-me"/> Remember me
</div>
<div class="form-group">
<input type="submit" id="login" value="登录" class="btn btn-primary" />
</div>
</form>
</div>
</body>
</html>
SpringSecurity 配置文件修改,添加remember me 功能配置:
package com.zzg.security.config;
import javax.sql.DataSource;
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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import com.zzg.security.provider.SpringSecurityProvider;
/**
* spring-security 配置文件
* @author zzg
*
*/
@Configuration
@EnableWebSecurity //开启Spring Security的功能
@EnableGlobalMethodSecurity(prePostEnabled=true)//开启注解控制权限
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* עSpringSecurityProvider
*/
@Autowired
private SpringSecurityProvider provider;
/**
*AuthenticationSuccessHandler
*/
@Autowired
private AuthenticationSuccessHandler securityAuthenticationSuccessHandler;
/**
* AuthenticationFailureHandler
*/
@Autowired
private AuthenticationFailureHandler securityAuthenticationFailHandler;
@Autowired
private DataSource dataSource; // 数据源
/**
* 定义需要过滤的静态资源(等价于HttpSecurity的permitAll)
*/
@Override
public void configure(WebSecurity webSecurity) throws Exception {
webSecurity.ignoring().antMatchers("static/css/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http.authorizeRequests()
.antMatchers("/login").permitAll() // 不需要权限路径
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login") // 登入页面
.successHandler(securityAuthenticationSuccessHandler) //自定义成功处理器
.failureHandler(securityAuthenticationFailHandler) //自定义失败处理器
.permitAll()
.and()
.logout();
// 当通过JDBC方式记住密码时必须设置 key,key 可以为任意非空(null 或 "")字符串,但必须和 RememberMeService 构造参数的
// key 一致,否则会导致通过记住密码登录失败
http.authorizeRequests()
.and()
.rememberMe()
.rememberMeServices(rememberMeServices())
.key("INTERNAL_SECRET_KEY");
}
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
// 自定义身份验证提供者
builder.authenticationProvider(provider);
}
/**
* 返回 RememberMeServices 实例
*
* @return the remember me services
*/
@Bean
public RememberMeServices rememberMeServices() {
JdbcTokenRepositoryImpl rememberMeTokenRepository = new JdbcTokenRepositoryImpl();
// 此处需要设置数据源,否则无法从数据库查询验证信息
rememberMeTokenRepository.setDataSource(dataSource);
// 启动创建表,创建成功后注释掉
// rememberMeTokenRepository.setCreateTableOnStartup(true);
// 此处的 key 可以为任意非空值(null 或 ""),单必须和起前面
// rememberMeServices(RememberMeServices rememberMeServices).key(key)的值相同
PersistentTokenBasedRememberMeServices rememberMeServices =
new PersistentTokenBasedRememberMeServices("INTERNAL_SECRET_KEY", provider.getUserDetailsService(), rememberMeTokenRepository);
// 该参数不是必须的,默认值为 "remember-me", 但如果设置必须和页面复选框的 name 一致
rememberMeServices.setParameter("remember-me");
return rememberMeServices;
}
}
注意:我这里拓展了自定义SpringSecurityProvider类,新增getUserDetailsService()方法,用于获取UserDetailsService 服务。
package com.zzg.security.provider;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import com.zzg.security.userservice.AuthUserDetails;
import com.zzg.security.userservice.CustomUserService;
/**
*自定义身份验证提供者
*
* @author zzg
*
*/
@Component
public class SpringSecurityProvider implements AuthenticationProvider {
@Autowired
private CustomUserService userDetailService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// TODO Auto-generated method stub
String userName = authentication.getName();
String password = (String) authentication.getCredentials();
// 查询用户权限信息
AuthUserDetails userInfo = (AuthUserDetails) userDetailService.loadUserByUsername(userName);
if (userInfo == null) {
throw new UsernameNotFoundException("");
}
// 密码判断
String encodePwd = DigestUtils.md5Hex(password).toUpperCase();
if (!userInfo.getPassword().equals(encodePwd)) {
throw new BadCredentialsException("");
}
return new UsernamePasswordAuthenticationToken(userInfo, userInfo.getPassword(),
userInfo.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
// TODO Auto-generated method stub
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
// 拓展获取用户查询服务
public UserDetailsService getUserDetailsService(){
return this.userDetailService;
}
}
补充:springsecurity remember-me 功能涉及数据库的建库脚本:
DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins` (
`username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`series` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`token` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`last_used` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`series`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
简单说明:springsecurity remember-me 功能流程和涉及Filter
首先看图:
1、通过上面的流程图可知,第一次发送认证请求,会被UsernamePasswordAuthenticationFilter拦截,然后身份认证。认证成功后,在AbstracAuthenticationProcessingFilter中,有个RememberMeServices接口。该接口默认实现类是NullRememberMeServices,这里会调用另一个实现抽象类AbstractRememberMeServices
2、调用AbstractRememberMeServices的loginSuccess方法。可以看到如果request中name为"remember-me"为true时,才会调用下面的onLoginSuccess()方法。这也是为什么上面登录页中的表单,name必须是"remember-me"的原因:
3、在Security中配置了rememberMe()之后, 会由PersistentTokenBasedRememberMeServices去实现父类AbstractRememberMeServices中的抽象方法。在PersistentTokenBasedRememberMeServices中,有一个PersistentTokenRepository,会生成一个Token,并将这个Token写到cookie里面返回浏览器。PersistentTokenRepository的默认实现类是InMemoryTokenRepositoryImpl,该默认实现类会将token保存到内存中。这里我们配置了它的另一个实现类JdbcTokenRepositoryImpl,该类会将Token持久化到数据库中
4、查看数据库中的persistent_logins 表数据:
5、发送第二次认证请求,只会携带Cookie。所以直接会被RememberMeAuthenticationFilter拦截,并且此时内存中没有认证信息。可以看到,此时的RememberMeServices是由PersistentTokenBasedRememberMeServices实现
6、在PersistentTokenBasedRememberMeServices中,调用processAutoLoginCookie方法,获取用户相关信息
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) + "'");
}
// 从Cookie中获取Series和Token
final String presentedSeries = cookieTokens[0];
final String presentedToken = cookieTokens[1];
//在数据库中,通过Series查询PersistentRememberMeToken
PersistentRememberMeToken token = tokenRepository
.getTokenForSeries(presentedSeries);
if (token == null) {
throw new RememberMeAuthenticationException(
"No persistent token found for series id: " + presentedSeries);
}
// 校验数据库中Token和Cookie中的Token是否相同
if (!presentedToken.equals(token.getTokenValue())) {
tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(
messages.getMessage(
"PersistentTokenBasedRememberMeServices.cookieStolen",
"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
}
// 判断Token是否超时
if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System
.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
}
if (logger.isDebugEnabled()) {
logger.debug("Refreshing persistent login token for user '"
+ token.getUsername() + "', series '" + token.getSeries() + "'");
}
// 创建一个新的PersistentRememberMeToken
PersistentRememberMeToken newToken = new PersistentRememberMeToken(
token.getUsername(), token.getSeries(), generateTokenData(), new Date());
try {
//更新数据库中Token
tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
newToken.getDate());
//重新写到Cookie
addCookie(newToken, request, response);
}
catch (Exception e) {
logger.error("Failed to update token: ", e);
throw new RememberMeAuthenticationException(
"Autologin failed due to data access problem");
}
//调用UserDetailsService获取用户信息
return getUserDetailsService().loadUserByUsername(token.getUsername());
}
至此用户remember 相关逻辑和涉及核心代码讲解完毕。
1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。