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. 系统学习scala--基础

    scala基础 安装scala(不推荐使用最新版本,2.11.x够用了) scala官网 2.11.12版本下载页面 这里我选择2.11.12版本,在下载页面往下拉,选择scala-2.11.12.m ...

  2. spring中<context:property-placeholder/>一个坑

    <context:property-placeholder location="classpath:db.properties" ></context:prope ...

  3. Java Web项目部署到阿里云服务器(ECS)

    本篇随笔只是记录博主第一次将自己的Java项目部署到阿里云服务器的大致过程,具体细节还请参考别的博文. 一.项目介绍 我做的项目是利用maven项目构建工具进行搭建基于SSM框架的代码共享管理系统,主 ...

  4. linux常用命令---系统辅助命令

    系统辅助命令

  5. Git基本操作命令合集

    平时自己敲敲代码,使用Git命令也渐渐多了起来.使用起来的确很方便,今天来分享下Git基本概念和本地代码提交到github上的过程,很简单的,多操作几次就会了. Git定义 Git 是一个开源的分布式 ...

  6. ftp服务器搭建(二)

    1.已经安装好了vsftpd  进入到根目录下的/etc目录 ls查看一下 2.拷贝一下上面的两个配置文件 我拷贝到了我新建的目录中了 3.查看现在的网络连接方式——我的是-net方式 当然其他方式也 ...

  7. 蓝桥杯 试题 历届试题 填字母游戏 博弈+dfs剪枝

    问题描述 小明经常玩 LOL 游戏上瘾,一次他想挑战K大师,不料K大师说: “我们先来玩个空格填字母的游戏,要是你不能赢我,就再别玩LOL了”. K大师在纸上画了一行n个格子,要小明和他交替往其中填入 ...

  8. channelartlist标签的使用

    用来获取当前频道的下级栏目的内容列表标签 . type=“top”表示顶级栏目 ,typeid='top' 限制上级栏目ID:如果只要调用其中几个频道的内容可以用{dede:channelartlis ...

  9. 关于mobileSelect.js日期数据获取封装,动态时间,封装

    传入起始年份和起始月份 得到 插件的标准格式放到 效果 let getAllDatas = (stime, etime) => { //接收传进来的参数月份 var data_M = etime ...

  10. 循序渐进VUE+Element 前端应用开发(2)--- Vuex中的API、Store和View的使用

    在我们开发Vue应用的时候,很多时候需要记录一些变量的内容,这些可以用来做界面状态的承载,也可以作为页面间交换数据的处理,处理这些内容可以归为Vuex的状态控制.例如我们往往前端需要访问后端数据,一般 ...