SpringSecurity 实现rember me 功能解析

java哥 阅读:216 2021-03-31 17:20:03 评论:0

添加该功能是在原有功能上新增功能: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.作者投稿可能会经我们编辑修改或补充。

发表评论
搜索
排行榜
KIKK导航

KIKK导航

关注我们