spring security提供了"记住我"的功能,来完成用户下次访问时自动登录功能,而无需再次输入用户名密码。 下面,我们来通过代码演示该功能的实现——主要是通过配置remember-me标签。
我们通过如下的配置过程来实现“记住我”的功能:
1、搭建maven项目(web工程),引入功能的相关依赖
2、配置web.xml中的spring、springsecurity的加载、springmvc的DispatcherServlet
3、配置spring、springmvc、springsecurity的配置文件
4、提供UserDetailService接口的实现类,采用写死数据完成认证用户的封装
5、controller代码提供查询返回当前登录人姓名的功能
6、准备登录页面login.html以及登录成功页面index.html和登录失败页面failer.html
7、测试“记住我”
1、搭建maven项目(web工程),引入功能的相关依赖
maven工程打包方式选择war,同时需要在项目pom做如下配置(只提供主要的依赖和插件,其他的可以参见附件代码):
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.3.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>80</port>
</configuration>
<version>2.2</version>
</plugin>
</plugins>
</build>
2、配置web.xml中的spring、springsecurity的加载、springmvc的DispatcherServlet
<!-- 前端控制器(加载classpath:springmvc.xml 服务器启动创建servlet) -->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 配置初始化参数,创建完DispatcherServlet对象,加载springmvc.xml配置文件 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<!-- 服务器启动的时候,让DispatcherServlet对象创建 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
还需要在web.xml中配置一个filter——springsecurity安全框架好,这个filter离不了!
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3、配置spring、springmvc、springsecurity的配置文件
——此处配置省略spring、springmvc的内容,可以参见附件,重点说明springsecurity配置文件内容
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
[url=http://www.springframework.org/schema/beans/spring-beans.xsd]http://www.springframework.org/schema/beans/spring-beans.xsd[/url]
[url=http://www.springframework.org/schema/security]http://www.springframework.org/schema/security[/url]
[url=http://www.springframework.org/schema/security/spring-security.xsd]http://www.springframework.org/s ... spring-security.xsd[/url]">
<!-- 配置不拦截的资源 -->
<security:http pattern="/login.html" security="none"/>
<security:http pattern="/failer.html" security="none"/>
<security:http auto-config="true" use-expressions="false">
<!-- 配置具体的拦截的规则 pattern="请求路径的规则" access="访问系统的人,必须有ROLE_ADMIN的角色" -->
<security:intercept-url pattern="/**" access="ROLE_ADMIN"/>
<!-- 定义跳转的具体的页面 -->
<security:form-login
login-page="/login.html"
login-processing-url="/login"
default-target-url="/index.html"
authentication-failure-url="/failer.html"
always-use-default-target="true"
/>
<!--关键点:remember-me配置后,启用RememberMeAuthenticationFilter过滤功能;
首次登陆时,如果remember-me标记传入,则会将认证成功的信息(用户名、有效期、密码、安全框架提供的key)加密后回写到cookie;
下次登陆时,请求中会将cookie带过去RememberMeAuthenticationFilter将其解析后,得到其中的用户名,查询用户信息后,再次将查询到的用户信息加密,将加密后的字符串对比cookie中的字符串
如果一致,设置认证成功;如果不一致,认证失败,抛出异常-->
<security:remember-me remember-me-parameter="remember-me" user-service-ref="userService"/>
<!-- 关闭跨域请求 -->
<security:csrf disabled="true"/>
<!-- 配置退出 -->
<security:logout invalidate-session="true" logout-url="/logout" logout-success-url="/login.html"/>
</security:http>
<!--为认证管理器配置认证提供者:自定义userDetailService-->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userDetailService">
</security:authentication-provider>
</security:authentication-manager>
<bean id="userService" class="com.itheima.security.service.UserDetailServiceImpl"/>
</beans>
4、提供UserDetailService接口的实现类,采用写死数据完成认证用户的封装
在上述配置文件中,有一个bean为UserDetailServiceImpl,要想完成自定义的用户认证功能,则需要让该类实现UserDetailService接口
@Service("userDetailService")
public class UserDetailServiceImpl implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//此处写死用户名、密码和权限列表
DBUser dbUser = getDBUserByUsername(username);
//封装用户权限列表:写死为拥有ROLE_ADMIN角色
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + dbUser.getRole()));
//将封装好的User返回
return new User(dbUser.getUsername(),"{noop}"+dbUser.getPassword(),authorities);
}
/**
* 模拟根据用户名查询数据库中的用户数据——此处假设输入的用户名永远正确,密码都是123456
* @param username
* @return
*/
private DBUser getDBUserByUsername(String username){
return new DBUser(username,"123456","ADMIN");
}
}
5、controller层提供类,提供查询返回当前登录人姓名的方法
@RestController
public class SecurityController {
@RequestMapping("getUsername")
public String getUsername(){
//得到当前认证对象的用户名并返回
return SecurityContextHolder.getContext().getAuthentication().getName();
}
}
6、准备登录页面login.html以及登录成功页面index.html和登录失败页面failer.html
6.1 login.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<!--注意:action="login" method为post-->
<form action="login" method="post">
<!--注意:name="username"-->
用户名:<input name="username" type="text"/><br>
<!--注意:name="password"-->
密 码:<input name="password" type="password"/>
<hr>
<!--注意:name="remember-me" 与后台remember-me标签配置的remember-me-parameter一致-->
<input type="checkbox" name="remember-me" value="true"/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
6.2 index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>欢迎页面</title>
<script src="jquery-2.2.3.min.js"></script>
</head>
<body>
<span id="nameId"></span>济南欢迎您!
<a href="logout">退出</a>
<script>
$(function(){
//发送异步请求,获取当前登录用户名
$.ajax({
url:'getUsername',
type:"post",
success:function(data){
$("#nameId").html(data);
}
})
})
</script>
</body>
</html>
6.3 failer.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>失败页面</title>
</head>
<body>
欢迎下次再来济南!
</body>
</html>
7、测试“记住我”
——第一次访问首页(index.html),会跳转到登录页面,在登录页面记得勾选那个复选框(remember-me)
——将浏览器直接关闭,再打开后访问首页(index.html),,会发现,本次访问无需登录即可访问
==================================================================================
下面,我们来简单了解一下在配置文件中添加remember-me元素之后,springsecurity框架做的事情:
1.在首次登录时,会经过remember-me对应的过滤器RememberMeAuthenticationFilter,在调用到userDetailService获取用户数据并认证成功之后,会经过TokenBasedRememberMeServices的onLoginSuccess方法,如下
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = this.retrieveUserName(successfulAuthentication);
String password = this.retrievePassword(successfulAuthentication);
if (!StringUtils.hasLength(username)) {
this.logger.debug("Unable to retrieve username");
} else {
if (!StringUtils.hasLength(password)) {
//调用自定义的userDetailService,得到返回的UserDetail对象
UserDetails user = this.getUserDetailsService().loadUserByUsername(username);
password = user.getPassword();
if (!StringUtils.hasLength(password)) {
this.logger.debug("Unable to obtain password for user: " + username);
return;
}
}
int tokenLifetime = this.calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
expiryTime += 1000L * (long)(tokenLifetime < 0 ? 1209600 : tokenLifetime);
//将时长、用户名、密码通过md5加密,得到签名字符串
String signatureValue = this.makeTokenSignature(expiryTime, username, password);
//新建cookie并将其写入到response中
this.setCookie(new String[]{username, Long.toString(expiryTime), signatureValue}, tokenLifetime, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
}
}
}
本次记录的签名值截图如下:
this.setCookie方法会进一步将编码形成cookieValue:
protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request, HttpServletResponse response) {
//将用户名、密码、签名组成的tokens编码后,得到cookieValue
String cookieValue = this.encodeCookie(tokens);
Cookie cookie = new Cookie(this.cookieName, cookieValue);
cookie.setMaxAge(maxAge);
cookie.setPath(this.getCookiePath(request));
//...省略部分代码
response.addCookie(cookie);
}
保存的cookie值如下:
也可以在浏览器抓包中看到:
2.关闭浏览器再次打开时,该cookie在存活时间内,会跟着请求带到后台,再次经过RememberMeAuthenticationFilter过滤器,执行doFilter方法:
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) {
//autoLogin方法来读取cookie,校验cookie中保存的数据和数据库中查询的是否一致
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
//省略部分代码
chain.doFilter(request, response);
}
}
在autoLogin方法中进行校验:
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
//获取cookieValue
String rememberMeCookie = this.extractRememberMeCookie(request);
//省略很多代码
//将cookieValue进行解析,得到其中的用户名、有效期以及签名
String[] cookieTokens = this.decodeCookie(rememberMeCookie);
//校验cookieValue中的签名
user = this.processAutoLoginCookie(cookieTokens, request, response);
this.userDetailsChecker.check(user);
this.logger.debug("Remember-me cookie accepted");
//经过验证后,将用户写如认证主体中
return this.createSuccessfulAuthentication(request, user);
//省略很多代码
this.cancelCookie(request, response);
return null;
}
解析cookieValue之后的cookieTokens内容如下:
进一步的验证在processAutoLoginCookie方法中进行:
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
//省略部分代码
//根据解析出来的用户名查询数据库用户
UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(cookieTokens[0]);
//将有效期以及查询出的数据库用户用户名、密码再次通过MD5加密,得到新的一个签名(expectedTokenSignature)
String expectedTokenSignature = this.makeTokenSignature(tokenExpiryTime, userDetails.getUsername(), userDetails.getPassword());
//判断新的签名和通过cookieVlaue解析出来的签名是否一致
if (!equals(expectedTokenSignature, cookieTokens[2])) {
throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2] + "' but expected '" + expectedTokenSignature + "'");
} else {
//如果一致,返回该用户
return userDetails;
}
}
经过加密之后新的签名值截图如下:
新的签名值和编码到cookieValue中的签名完全一致!
本次记住我功能配置使用的是较简单——仅通过cookie没有通过数据库记录的——功能实现,源码中挑选了其中几个关键点来加以说明“记住我”功能的实现过程,欢迎更深入的交流。