SpringSecurity(2)---记住我功能实现

上一篇博客实现了认证+授权的基本功能,这里在这个基础上,添加一个 记住我的功能

上一篇博客地址:SpringSecurity(1)---认证+授权代码实现

说明:上一遍博客的 用户数据用户关联角色 的信息是在代码里写死的,这篇将从mysql数据库中读取。

一、数据库建表

这里建了三种表

一般权限表有四张或者五张,这里有关 角色关联资源表 没有创建,角色和资源的关系依旧在代码里写死。

建表sql

/*创建用户表*/
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; /*创建j角色表*/
CREATE TABLE `roles` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8; /*创建用户关联角色表*/
CREATE TABLE `roles_user` (
`id` int NOT NULL AUTO_INCREMENT,
`rid` int DEFAULT '2',
`uid` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=133 DEFAULT CHARSET=utf8; /*这里密码对应的明文 还是123456*/
INSERT INTO `user` (`id`, `username`, `nickname`, `password`, `enabled`)
VALUES
(1, '小小', '小小', 'e10adc3949ba59abbe56e057f20f883e', 1); /*三种角色*/
INSERT INTO `roles` (`id`, `name`)
VALUES
(1, '校长'),
(2, '教师'),
(3, '学生'); /*小小用户关联了 教师和校长角色*/
INSERT INTO `roles_user` (`id`, `rid`, `uid`)
VALUES
(1, 2, 1),
(2, 3, 1);

说明:这里数据库只有一个用户

用户名 :小小

密码:123456

她所拥有的角色有两个 教师学生

二、Spring Security的记住我功能基本原理

概念 记住我在登陆的时候都会被用户勾选,因为它方便地帮助用户减少了输入用户名和密码的次数,用户一旦勾选记住我功能那么 当服务器重启后依旧可以不用登陆就可以访问

Spring Security的“记住我”功能的基本原理流程图如下所示:

这里大致流程如下:

第一次登陆

用户请求的时候 remember-me参数为true 时,用户先进行 认证+授权过滤器。然后走记住我过滤器这里需要做两,这里主要做两件事。

1.将Token数据存入数据库 2.将token数据存入cookie中。

服务重启后

如果服务重启的话,那么之前的session信息已经不在了,但是cookie中的Token还是存在的。所以当用户重启后去访问需要认证的接口时,会先通过cookie中的Token

去数据库查询这条Token信息,如果存在那么在通过用户名去查询数据库获取当前用户的信息。

三、代码实现

因为上面项目已经完成了整个授权+认证的过程,那么这里就很简单添加一点点代码就可以了。

在WebSecurityConfig中添加一个Bean,配置完这个Bean就基本完成了 记住我 功能的开发,然后在将这个Bean设置到configure方法中即可。

    @Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}

上面的代码 tokenRepository.setCreateTableOnStartup(true) ;是自动创建Token存到数据库时候所需要的表,这行代码只能运行一次,如果重新启动数据库,

必须删除这行代码,否则将报错,因为在第一次启动的时候已经创建了表,不能重复创建。保险起见我们还是注释掉这段代码,手动建这张表。

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;

在配置里再加上这些就可以了。

四、测试

主要测试两个点地方,

1、当我登陆时选择记住我功能,看下数据库persistent_logins是否有一条token记录
2、当使用记住我功能后,关闭服务器在重启服务器,不再登陆直接访问需要认证的接口,看是否能够访问成功。

1、首次登陆

我们在看数据库token表

很明显新增了一条token数据。

2、重启服务器

这个时候我们重启服务器访问需要认证的接口

发现就算重启也不需要重启登陆就可以反问需要认证的接口。

五、源码分析

同样这里也分为两部分 1、第一次登陆源码流程。 2、重启后未认证再去访问需要认证的接口源码流程。

1、首次登陆源码流程

第一步

当用户发送登录请求的时候,首先到达的是UsernamePasswordAuthenticationFilter这个过滤器,然后执行attemptAuthentication方法的代码,代码如下图所示:

 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//从这里可以看出登陆需要post提交
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
} if (password == null) {
password = "";
} username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}

之后所走的流程就是 ProviderManager的authenticate方法 ,之后再走AbstractUserDetailsAuthenticationProvider的authenticate方法,再走DaoAuthenticationProvider的方法retrieveUser方法

 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection(); try {
//这里就走我们自定义的获取用户认证和授权信息的代码了
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}

这样一来,认证的流程就已经走完了。那就要走记住我功能的过滤器了。

第二步

验证成功之后,将进入AbstractAuthenticationProcessingFilter 类的successfulAuthentication的方法中,首先将认证信息通过代码

SecurityContextHolder.getContext().setAuthentication(authResult);将认证信息存入到session中,紧接着这个方法中就调用了rememberMeServices的loginSuccess方法

 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);
//记住我
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
} this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

再走PersistentTokenBasedRememberMeServices的onLoginSuccess方法

    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 {
//这里就是关键的两步 1、将token存入到数据库 2、将token存入cookie中
this.tokenRepository.createNewToken(persistentToken);
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
} }

这个方法中调用了tokenRepository来创建Token并存到数据库中,且将Token写回到了Cookie中。到这里,基本的登录过程基本完成,生成了Token存到了数据库,

且写回到了Cookie中。

2、第二次访问

重启项目,这时候服务器端的session已经不存在了,但是第一次登录成功已经将Token写到了数据库和Cookie中,直接访问一个服务,并且不输入用户名和密码。

第一步

首先进入到了RememberMeAuthenticationFilter的doFilter方法中,这个方法首先检查在session中是否存在已经验证过的Authentication了,如果为空,就进行下面的

RememberMe的验证代码,比如调用rememberMeServices的autoLogin方法,代码如下:

    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) {
//走记住我流程
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
//省略不重要的代码
chain.doFilter(request, response);
} else {
chain.doFilter(request, response);
}
}

我们在看this.rememberMeServices.autoLogin(request, response)方法。最终实现在AbstractRememberMeServices的autoLogin方法

    public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
//1、获取token
String rememberMeCookie = this.extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
} else { UserDetails user = null;
try {
String[] cookieTokens = this.decodeCookie(rememberMeCookie);
//这步是关键
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;
}
this.cancelCookie(request, response);
return null;
}
}
}

我们在看 this.processAutoLoginCookie(cookieTokens, request, response);在PersistentTokenBasedRememberMeServices中实现,到这一步就已经很明白了

 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];
//1、去token表中查询token
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
//2校验数据
} 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."));
//3、查看token是否过期
} 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 {
//4、更新这条token 没更新一次有效时间就都变成了之间设置的时间
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");
}
//5、这里拿着用户名 就又获取当前用户的认证和授权信息
return this.getUserDetailsService().loadUserByUsername(token.getUsername());
}
}
}

这样整个流程就完成了,我们可以看出源码的过程和上面图片展示的流程还是非常像的。

别人骂我胖,我会生气,因为我心里承认了我胖。别人说我矮,我就会觉得好笑,因为我心里知道我不可能矮。这就是我们为什么会对别人的攻击生气。
攻我盾者,乃我内心之矛(18)

SpringSecurity(2)---记住我功能实现的更多相关文章

  1. SpringSecurity之记住我功能的实现

    Spring security记住我基本原理: 登录的时候,请求发送给过滤器UsernamePasswordAuthenticationFilter,当该过滤器认证成功后,会调用RememberMeS ...

  2. SpringSecurity实现记住我功能

    ⒈表单添加 <form action="/authentication/form" method="post"> <table> < ...

  3. SpringBoot + Spring Security 学习笔记(四)记住我功能实现

    记住我功能的基本原理 当用户登录发起认证请求时,会通过UsernamePasswordAuthenticationFilter进行用户认证,认证成功之后,SpringSecurity 调用前期配置好的 ...

  4. JavaWeb-SpringSecurity记住我功能

    系列博文 项目已上传至guthub 传送门 JavaWeb-SpringSecurity初认识 传送门 JavaWeb-SpringSecurity在数据库中查询登陆用户 传送门 JavaWeb-Sp ...

  5. java实现记住密码功能(利用cookie)

    <br> <input type="text" id="userName" name="userName" value=& ...

  6. 通过sharedpreferences实现记住密码功能

    通过sharedpreferences实现记住密码功能

  7. jquery.cookie.js 操作cookie实现记住密码功能的实现代码

    jquery.cookie.js操作cookie实现记住密码功能,很简单很强大,喜欢的朋友可以参考下.   复制代码代码如下: //初始化页面时验证是否记住了密码 $(document).ready( ...

  8. cookie记住密码功能

    很多门户网站都提供了记住密码功能,虽然现在的浏览器都已经提供了相应的记住密码功能 效果就是你每次进入登录页面后就不需要再进行用户名和密码的输入: 记住密码功能基本都是使用cookie来进行实现的,因此 ...

  9. 【原创】js中利用cookie实现记住密码功能

    在登录界面添加记住密码功能,我首先想到的是在java后台中调用cookie存放账号密码,大致如下: HttpServletRequest request HttpServletResponse res ...

随机推荐

  1. sqli-labs之Page-2

    第二十一关:base64编码的cooki注入 YOUR COOKIE : uname = YWRtaW4= and expires: Tue 10 Mar 2020 - 03:42:09 注:YWRt ...

  2. 黑马程序员_毕向东_Java基础视频教程——java语言组成部分(随笔)

    java语言组成部分 Java是一种强类型语言,所谓强类型语言就是对具体的数据进行不同的定义.对类型的划分的十分细致,对内存中分配了不同大小的内u你空间 关键字 标识符 注释 变量和常量 运算符 语句 ...

  3. 曹工谈Spring Boot:Spring boot中怎么进行外部化配置,一不留神摔一跤;一路debug,原来是我太年轻了

    spring boot中怎么进行外部化配置,一不留神摔一跤:一路debug,原来是我太年轻了 背景 我们公司这边,目前都是spring boot项目,没有引入spring cloud config,也 ...

  4. 观察者模式C++实现

    #include <iostream> #include <vector> #include <string> using namespace std; typed ...

  5. 2.4 Go与包

    1.1Go与包 1.1.1.  Go与包 1)开发中,往往要在不同的文件中调用其他文件的函数 2)Go代码最小粒度单位是"包" 3)Go的每一个文件都属于一个包,通过package ...

  6. nginx配置之禁止访问和404错误功能配置

    禁止访问功能配置 nginx.conf中的http{}中的server{}的location ..{}中: location / { #拒绝访问,192.168.16.0网段的同学都无法访问 /24是 ...

  7. 201771010128王玉兰《面向对象与程序设计(java)第十五周学习总结》

    第一部分:理论知识 一:JAR文件 Java程序的打包:程序编译完成后,程序员 将.class文件压缩打包为.jar文件后,GUI界面 程序就可以直接双击图标运行. jar文件(Java归档)既可以包 ...

  8. SQLSTATE[42S01]: Base table or view already exists: 1050 Table 'xxx' already exists

    字面意思 xxx表已存在. 在使用laravel  写同步结构的时候 最好习惯性写个if语句判定是否存在 // 判断数据表是否存在 Schema::hasTable('table'); // 判断数据 ...

  9. Python对象组合

    一个类的对象作为另一个类的对象的属性,称为类的组合. 即 class1.instance1.property = class2.instance 组合也是代码重用的重要方式之一. 先定义三个类:人.汽 ...

  10. 选择器&隔行换色

    选择器的使用理解为:执行jQuery核心函数,传入选择器的字符串    $( ... ) 基本选择器 <!DOCTYPE html> <html> <head> & ...