百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

SpringSecurity记住我功能如何实现含源码分析

toyiye 2024-06-21 12:19 11 浏览 0 评论

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没有通过数据库记录的——功能实现,源码中挑选了其中几个关键点来加以说明“记住我”功能的实现过程,欢迎更深入的交流。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码