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 相关逻辑和涉及核心代码讲解完毕。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值