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. spring mvc json返回防止乱码

    乱码问题 乱码一直是编程的常见问题,spring mvc 返回json数据时可能导致乱码,需要在controller中添加如下代码: @RequestMapping("/test" ...

  2. JavaScript和TypeScript的区别和联系

    转载自:http://web.jobbole.com/93618/?utm_source=group.jobbole.com&utm_medium=relatedArticles JavaSc ...

  3. 聚类算法——DBSCAN算法原理及公式

    聚类的定义 聚类就是对大量未知标注的数据集,按数据的内在相似性将数据集划分为多个类别,使类别内的数据相似度较大而类别间的数据相似度较小.聚类算法是无监督的算法. 常见的相似度计算方法 闵可夫斯基距离M ...

  4. 【Django】rest_framework 序列化自定义替换返回值

    # 序列化设置 class PagerSerialiser(serializers.ModelSerializer): name = serializers.CharField(source=&quo ...

  5. 运行web容器实例

  6. 必须使用角色管理工具 安装或配置microsoft.net framework 3.5

    windows server 2008安装sql server 2012后报错,要求安装microsoft.net framework 3.5 sp1 但安装时提示,必须使用角色管理工具 安装或配置m ...

  7. System.Web.mail ----虚拟发件人发送邮件

     转载别人的 使用SMTP发送邮件   说到邮件发送,先提一下SMTP. SMTP的全称是“Simple Mail Transfer Protocol”,即简单邮件传输协议.它是一组用于从源地址到目的 ...

  8. dij+DP

    众所周知,蒜蒜是一名热爱工作的好员工,他觉得时间就是金钱,做事情总是争分夺秒. 这天晚上,蒜蒜一个人去吃晚饭.不巧的是,吃完饭以后就开始下雨了,蒜蒜并没有带雨伞出来.但是蒜蒜热爱工作,工作使他快乐,他 ...

  9. Gym101612H Hidden Supervisors

    题目链接:https://vjudge.net/problem/Gym-101612H 知识点: 贪心 解题思路: 我们称除了以 \(1\) 号结点为根的树以外的树为 “其他树”. 对于每一棵树,先自 ...

  10. XXX_ProductCRUD的项目结构与配置文件

    MVC_ProductCRUD                                                       Hibernate_ProductCRUD 项目结构    ...