Spring Security
一、Spring Security引入
1. 简介
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制(认证和授权)框架。它是保护基于 Spring 应用程序的事实标准。
2. 认证授权
-
认证授权实现平台所有用户的身份认证与用户授权功能。
2.1 什么是用户认证
-
认证:Authentication
验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。
通常用户认证一般要求用户提供用户名和密码,手机号验证码等,系统通过校验来完成认证过程。
比如:创建订单操作,需要学生用户首先登录系统,才可以创建订单。
2.2 什么是用户授权
-
授权:Authorization
用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问。
在一个系统中,不同用户所具有的权限是不同的。比如对一个资源来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
比如:用户去发布课程,系统首先进行用户身份认证,认证通过后继续判断用户是否有发布课程的权限,如果没有权限则拒绝继续访问系统,如果有权限则继续发布课程。
3. 常见认证方式
3.1 Http basic 认证
http basic auth 通过使用Restful风格开发,每次请求服务器都携带用户名和密码。这种方式使用越来越少,因为不安全。认证过程如下:
3.2 Session 认证
会话认证就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器段创建了一个Cookie对象,通过客户端带上来Cookie对象来与服务器端Session对象匹配实现用户状态管理的。
3.3 Token 认证
使用基于Token的身份验证方法,在服务端不需要存储用户的登录记录。大概流程如下:
-
客户端使用用户名和密码请求登录
-
服务端收到请求,去验证用户名与密码
-
验证成功后,服务器颁发一个Token,再把这个Token发送给客户端
-
客户端收到Token以后可以把它存储起来
-
客户端每次向服务器端请求资源的时候需要带着服务端颁发的Token
-
服务端收到请求,然后去验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求的数据。
3.4 OAuth2 认证
例如:微信扫码认证,这是一种第三方认证的方式,这种认证方式是基于OAuth2协议实现,
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的。互联网很多服务如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
二、Spring Security介绍
Spring Security是一个高度自定义的安全框架。利用Spring IoC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。
两个主要区域是“认证(authentication)”和“授权(authorization)”。这两点也是Spring Security重要核心功能。
“认证”,是建立一个他声明的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。
“授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否有权限去做某些事情。
认证功能几乎是每个项目都要具备的功能,并且它与业务无关,市面上有很多认证框架,如:Spring Security、Apache Shiro等。
Spring Security是spring家族的一份子且可以和Spring应用很好的集成。
总结:Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,它是一个专注于为 Java 应用程序提供身份验证和授权的框架。
Spring Security项目主页:https://spring.io/projects/spring-security
Spring cloud Security: https://spring.io/projects/spring-cloud-security
三、认证快速入门
1. 引入依赖
<!-- 引入web项目启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 引入Spring Security启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
也可以在创建springboot项目时直接勾选相关的依赖
2. 创建控制器
@RestController public class UserController { @RequestMapping("show") public String show(){ return "Hello World!"; } }
3. 访问资源
导入spring-boot-starter-security启动器后,Spring Security已经生效,默认拦截全部请求,如果用户没有登录,跳转到内置登录页面。
Username默认为:user
Password为SpringSecurity自动生成的,每次都不一样:
4. 完成登录
5. 自定义登录用户名和密码
application.yml:
spring: security: user: name: root password: root
四、自定义认证逻辑
1. UserDetailsService接口
1.1 介绍
当什么也没有配置的时候,账号和密码是由Spring Security定义生成的,存储在内存中。
而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们需要通过自定义逻辑控制认证(登录)逻辑。
如果需要自定义逻辑时,只需要实现UserDetailsService接口即可。
接口定义如下:
-
默认策略:浏览器发起请求,经过拦截器:
-
没有认证(没有登录),执行UserDetailsService接口实现类InMemoryUserDetailsManager中loadUserByUsername()方法,将输入的用户名和内存中用户名完成比较,用户名比较通过后,再比较输入的密码和内存中的密码。
-
-
用户名和密码需要从数据库查询后再完成比较,重写UserDetailsService接口中的loadUserByUsername()方法即可。
-
思路分析:
没有认证(没有登录),执行UserDetailsService接口自定义实现类中loadUserByUsername()方法,根据用户名在数据库中查询数据,查询到了数据,再完成密码的比较。
-
1.2 返回值UserDetails
1.2.1 UserDetails接口
1.2.2 User实现类
Spring Security中提供了如下的实例。我们可以直接使用提供的User类或自定义类实现UserDetails接口。
注意User的全限定路径:org.springframework.security.core.userdetails.User 此处经常和系统中自己开发的User类弄混。
构造方法:
构造方法有两个,调用其中任何一个都可以实例化UserDetails实现类User类的实例。而三个参数的构造方法实际上也是调用7个参数的构造方法。

/** * Calls the more complex constructor with all boolean arguments set to {@code true}.调用更复杂的构造函数,所有布尔参数都设置为true。 */ public User(String username, String password, Collection<? extends GrantedAuthority> authorities) { this(username, password, true, true, true, true, authorities); } public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor"); this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); }
-
username:用户名。
-
password:密码。
从数据库中查询到的密码,Spring Security会根据User中的password和客户端传递过来的password进行比较。相同,认证通过。不相同,认证失败。
-
authorities:用户具有的权限。此处不允许为null。
authorities中的权限是后面学习授权需要的,存储数据库中查询到的用户权限。
1.3 声明的异常UsernameNotFoundException
UsernameNotFoundException 用户名没有发现异常。在loadUserByUsername中是需要通过自己的逻辑从数据库中取值。如果通过用户名没有查询到对应的数据,抛出UsernameNotFoundException,系统就知道用户名没有查询到。
2. 环境准备
2.1 数据库表

-- ---------------------------- -- Table user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户编号', `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户账号', `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户密码', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `username`(`username`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; # 密码123 INSERT INTO `user` VALUES (1, 'zs', '$2a$10$TaQcX9rbKxlr8tldLdsOze.BXZNrf/arDl3SI.UqK.b3vIIl7GyB.'); # 密码456 INSERT INTO `user` VALUES (2, 'ls', '$2a$10$OMSqM25kctdfs1o4QJ/WVeMEAhDkVngV2li/F6QIkbBZuVACwAF3i'); -- ---------------------------- -- Table role -- ---------------------------- DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色编号', `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色名称', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; INSERT INTO `role` VALUES (1, '管理员'); INSERT INTO `role` VALUES (2, '普通用户'); -- ---------------------------- -- Table role_user -- ---------------------------- DROP TABLE IF EXISTS `role_user`; CREATE TABLE `role_user` ( `uid` bigint(20) NOT NULL COMMENT '用户编号', `rid` bigint(20) NOT NULL COMMENT '角色编号', PRIMARY KEY (`uid`, `rid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; INSERT INTO `role_user` VALUES (1, 1); INSERT INTO `role_user` VALUES (2, 2); -- ---------------------------- -- Table menu -- ---------------------------- DROP TABLE IF EXISTS `menu`; CREATE TABLE `menu` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单编号', `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '菜单名称', `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '菜单地址', `permission` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '菜单访问权限', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; INSERT INTO `menu` VALUES (1, '查询学生', 'stu/select', 'stu:select'); INSERT INTO `menu` VALUES (2, '添加学生', 'stu/insert', 'stu:insert'); INSERT INTO `menu` VALUES (3, '修改学生', 'stu/update', 'stu:update'); INSERT INTO `menu` VALUES (4, '删除学生', 'stu/del', NULL); -- ---------------------------- -- Table role_menu -- ---------------------------- DROP TABLE IF EXISTS `role_menu`; CREATE TABLE `role_menu` ( `rid` bigint(20) NULL DEFAULT NULL COMMENT '角色编号', `mid` bigint(20) NULL DEFAULT NULL COMMENT '菜单编号' ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; INSERT INTO `role_menu` VALUES (1, 1); INSERT INTO `role_menu` VALUES (1, 5); INSERT INTO `role_menu` VALUES (1, 2); INSERT INTO `role_menu` VALUES (1, 3); INSERT INTO `role_menu` VALUES (1, 4);
2.2 引入依赖
<!-- mybatis-plus框架 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <!--mysql数据库库--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency>
2.3 配置文件

# 数据源配置 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: root mybatis-plus: type-aliases-package: com.xxx.pojo configuration: #mybatis-plus日志控制台输出 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: #关闭banner banner: false
2.4 实体类

@Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; @TableField(exist = false) private static final long serialVersionUID = 1L; }
2.5 UserMapper
-
入口类添加了MapperScan("com.xxx.mapper"),则不需要其它处理。否则在映射接口添加@Mapper。
public interface UserMapper extends BaseMapper<MyUser> { }
2.6 UserServiceImpl
@Service public class UserServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; /** * @param username 前端登录时提交的用户名 * @return 用户的认证信息,要求必须为UserDetails接口的实现类 * @throws UsernameNotFoundException 用户名没有找到时的异常 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, username); User myUser = userMapper.selectOne(wrapper); if (myUser == null){ throw new UsernameNotFoundException("用户名不存在"); } /* * org.springframework.security.core.userdetails.User为UserDetails接口的实现类。 * 也可以自定义的User实现UserDetail接口提供对应的属性。 * */ //参数一:用户名 参数二:密码 参数三:权限(暂时不授权,创建个没有元素集合。不能指定null) ArrayList<GrantedAuthority> list = new ArrayList<>(); org.springframework.security.core.userdetails.User user = new org.springframework.security.core.userdetails.User( myUser.getUsername(), myUser.getPassword(), list); return user; } }
2.7 UserController

@RestController @RequestMapping("user") public class UserController { @RequestMapping("show") public String show(){ return "Hello Show!"; } @RequestMapping("query") public String query(){ return "Hello Query!"; } }
2.8 登录时出现的异常
2.9 PasswordEncoder接口(密码编码器)
Spring Security要求容器中必须有PasswordEncoder接口实现类对象。
作用:密码校验和密码加密。
2.9.1 介绍
-
encode(): 将密码进行加密。可以用在注册用户中,将用户提交的密码加密后再存储到数据库中。
-
matches():用户登录时,将新提交的密码进行加密,加密后和数据库中存储的已经加密的密码进行校验。密码匹配,返回true;不匹配,返回false。
第1个参数:新提交的密码。第2个参数:数据库中存储的已经加密的密码。
-
upgradeEncoding():用于判断是否需要对密码进行再次加密,以使得密码更加安全, 默认:false不需要。
2.9.2 接口实现类
Spring security中有多种实现类完成密码加密:
-
MD5算法的Md5PasswordEncoder
MD5 是一种 128 位的哈希函数,常用于数据完整性校验和数字签名等方面。它将任意长度的信息映射为一个 128 位的摘要值,输出的值通常表示为一个 32 位的十六进制数。MD5 的安全性已经被破解,因此现在不再被广泛使用。
-
SHA 算法的ShaPasswordEncoder
SHA 系列算法有多个版本,比较常见的有 SHA-1、SHA-256、SHA-384 和 SHA-512。它们都是将输入数据映射为一个固定长度的输出摘要值。SHA-1 生成的摘要长度为 160 位,而 SHA-256、SHA-384 和 SHA-512 分别生成 256 位、384 位和 512 位的摘要值。SHA-256 是目前应用最广泛的 SHA 算法之一,它的输出长度和安全性都比 MD5 更高。
-
BCrypt算法的BCryptPasswordEncoder
Bcrypt 是一种密码哈希函数,可以用于对密码进行加密和验证。它是一种慢速哈希函数,其设计目的是为了防止暴力破解攻击。Bcrypt 还可以通过增加 salt(随机值)和工作因子(算法运行次数)来进一步增强安全性。
Bcrypt 算法的实现中,会将明文密码和 salt 进行混合,并经过多轮哈希计算,最终得到一个长度为 60 个字符的密文。这个密文包括算法标识符、salt、工作因子和哈希值等信息。在验证密码时,Bcrypt 会从密文中提取出 salt 和工作因子,然后使用相同的哈希算法和参数来计算输入密码的哈希值,最后比较计算出的哈希值和密文中的哈希值是否一致。
由于 Bcrypt 算法的计算量比较大,因此它可以有效地防止暴力破解攻击。同时,使用不同的 salt 和工作因子可以让相同的密码在不同的计算中生成不同的哈希值,进一步增加了破解难度。
BCryptPasswordEncoder是基于哈希函数实现的单向加密。可以通过strength控制加密强度,默认10。
BCryptPasswordEncoder是Spring Security官方推荐的密码解析器,平时多使用这个加密器。
2.10 创建配置类
-
创建配置类,将BCryptPasswordEncoder交由Spring容器管理。
-
com.xxx.config.SecurityConfig
@Configuration public class SecurityConfig { //密码比较,加密器 @Bean public PasswordEncoder getPwdEncoder() { return new BCryptPasswordEncoder(); } }
五、自定义登录页面
1. 创建登录页面
虽然Spring Security给我们提供了登录页面,但实际项目开发时,并不会使用默认的登录页面。所以Spring Security中不仅仅提供了登录页面,还支持用户自定义登录页面。
-
static/login.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录</title> </head> <body> <form action="login" method="post"> 用户名:<input type="text" name="username"/> <br> 密码:<input type="password" name="password"/> <br> <input type="submit" value="提交"/> </form> </body> </html>
注意:虽然自定义了登录页面,Spring Security依然要求使用默认登录页面先完成认证,认证后才允许访问自定义的登录页面。
2. 修改配置类
-
修改过滤器链中的配置
-
请求授权设置
-
login.html 没有认证,可以被访问
-
其它请求没有认证,不可以被访问
-
-
自定义的登录的请求能够被拦截器
-
登录请求被拦截后,Spring Security会获取登录请求的用户名和密码,获取后调用自定义UserDetailsService接口实现类UserServiceImpl,从数据库中根据获取的用户名进行查询和后续的密码校验。
-
注意:此过程不经过控制器。
-
设置登录页面的访问路径
-
设置登录请求的访问路径
-
也可以设置登录成功后请求的资源
-
也可以设置登录失败后请求的资源
-
-
自定义登录请求需要关闭CSRF
-
com.xxx.config.SecurityConfig
@Configuration public class SecurityConfig { //滤器链的相关设置 @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //请求授权设置。 /* * antMatchers():对指定的请求url进行控制 * permitAll():允许访问 * anyRequest():任意一个请求url的控制 * authenticated():认证后便可以访问 * */ http.authorizeRequests() .antMatchers("/login.html").permitAll() //login.html不需要被认证 .anyRequest().authenticated(); //其它任意的请求都必须被认证,必须认证(登录成功)后就可以直接访问 //Form表单提交请求的处理。(可以为传统form表单提交,ajax提交) http.formLogin() .loginPage("/login.html") //登录页面对应的地址,退出登录功能时会使用 .loginProcessingUrl("/user/login") //登录的url,SpringSecurity会拦截该请求,执行登录的处理 .successForwardUrl("/user/ok") //登录成功跳转的资源。默认只接收post请求 .failureForwardUrl("/user/err"); //登录失败跳转的资源 //关闭csrf防护 http.csrf().disable(); return http.build(); } //密码比较,加密器 @Bean public PasswordEncoder getPwdEncoder() { return new BCryptPasswordEncoder(); } }
3. 修改控制器类

@RestController @RequestMapping("user") public class UserController { @RequestMapping("show") public String show(){ return "Hello Show!"; } @RequestMapping("query") public String query(){ return "Hello Query!"; } @RequestMapping("ok") public String ok(){ return "登录成功"; } @RequestMapping("err") public String err(){ return "登录失败"; } }
4. 修改请求用户名和密码的参数名
4.1 源码分析
当进行登录时会执行UsernamePasswordAuthenticationFilter过滤器,将接收请求中用户名和密码后进行封装。
-
usernamePasrameter:用户名参数名。
-
passwordParameter:密码参数名。
-
postOnly=true:默认情况下只允许POST请求。
4.2 修改配置类
//Form表单提交请求的处理。(可以为传统form表单提交,ajax提交) http.formLogin() .loginPage("/login.html") //登录页面对应的地址 .loginProcessingUrl("/user/login") //登录的url,提交的请求为/login,SpringSecurity执行登录的处理 .successForwardUrl("/user/ok") //登录成功跳转的资源。默认只接收post请求 .failureForwardUrl("/user/err") //登录失败跳转的资源 .usernameParameter("uname") //自定义用户名参数名 .passwordParameter("pwd"); //自定义密码参数名
注意
http.authorizeRequests() .antMatchers("/login.html").permitAll()是设置访问"/login.html"不需要身份认证
而http.formLogin() .loginPage("/login.html") 是设置需要进行身份认证时自动跳转的页面
六、认证流程源码分析
-
入口为AbstractAuthenticationProcessingFilter过滤器的doFilter()方法。
1. UsernamePasswordAuthenticationFilter
-
用户提交登录请求后执行UsernamePasswordAuthenticationFilter
获取身份验证管理器的方法
这里获取到的是ProviderManager
2. AuthenticationManager
-
ProviderManager为AuthenticationManager接口的实现类实现了authenticate方法
进入AbstractUserDetailsAuthenticationProvider的authenticate()方法
进入到DaoAuthenticationProvider的retrieveUser()方法
3. DaoAuthenticationProvider
-
调用父类AbstractUserDetailsAuthenticationProvider中的authenticate方法
getUserDetalsService()返回类型为UserDetailsService,
这里返回的使我们自定义的UserDetailsService的实现类:UserServiceImpl,并执行loadUserByUsername()方法,这里是真正的去数据库查询。
-
接收到UserDetails接口实现类User对象后,返回到AbstractUserDetailsAuthenticationProvider的authenticate()方法继续进行相关验证
执行check()进行检查:
检查通过后返回,继续执行additionalAuthenticationChecks()方法做额外的身份验证
进入DaoAuthenticationProvider 的 additionalAuthenticationChecks()方法
-
认证通过后检查密码是否过期,没有过期会调用createSuccessAuthentication方法,在该方法中new 了一个 UsernamePasswordAuthenticationToken,因为到这里认证已经通过了,所以将 authorities(授权数据) 注入进去,并设置 authenticated 为 true,已经被认证。
4. AbstractAuthenticationProcessingFilter
方法执行完回到入口处,继续向下执行successfulAuthentication
5. 流程总结
-
用户在浏览器中随意输入一个URL。
-
Spring Security 会判断当前是否已经被认证(登录)如果已经认证,正常访问URL。如果没有被认证跳转到loginPage()对应的URL中,显示登录页面。
-
用户输入用户名和密码点击登录按钮后,发起请求。
-
如果url和loginProcessingUrl()一样才执行登录流程。否则需要重新认证。
-
执行登录流程时首先被UsernamePasswordAuthenticationFilter进行过滤,取出用户名和密码,放入到容器(UsernamePasswordAuthenticationToken)中。根据usernameParameter和passwordParameter获取用户名和密码,如果没有配置这两个方法,默认为请求参数名username和password,把UsernamePasswordAuthenticationToken 交给 AuthenticationManager对象进行匹配管理,在当前方法中会进行认证方式匹配(AuthenticationProvider )
-
我们使用表单是AbstractUserDetailsAuthenticationProvider,再次调用retrieveUser()执行自定义登录逻辑UserDetailsService的实现类。返回数据库中保存当前用户信息,然后调用三次检查方法
-
preAuthenticationChecks.check(user);(账号是否锁定,账号是否启用,账号是否过期)
-
additionalAuthenticationChecks(user,authentication);(密码是否为null,密码是否正确)
-
postAuthenticationChecks.check(user);(密码是否过期)
-
-
经过AbstractAuthenticationProcessingFilter中doFliter()进行跳转:
-
登录成功,执行onAuthenticationSuccess方法,重定向到指定的资源。
-
通过session存储用户的认证信息。一个客户端会分配一个线程,在seesion中存储时为了保证线程安全,使用ThreadLocal存储认证信息。
-
ThreadLocal:存储在ThreadLocal中的数据可以保证线程安全。
-
-
-
登录失败,执行onAuthenticationFailure方法,请求转发到指定的资源。
-
注意:如果没有配置成功和失败转发URL,会跳转到用户访问的URL。
-
七、退出登录
1. 创建退出登录
-
退出登录的请求路径为logout,会被过滤器拦截,拦截后会跳转到登录页面。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <a href="/logout">退出登录</a> </body> </html>
2. 设置退出登录
-
注意:退出登录后会清除认证信息,只有配置了放行的资源才能够跳转。
再配置类中添加:
http.logout() .logoutUrl("/mylogout") //自定义退出访问的路径 .logoutSuccessUrl("/user/login.html"); //自定义退出成功后跳转的资源
3. logout流程源码分析
-
入口为LogoutFilter过滤器中的doFilter()方法。
八、Remember Me
-
Spring Security 中Remember Me为 "记住我" 功能,Spring Security会自动把用户信息存储到数据源中,以后就可以不登录进行访问。
-
用户只需要在登录表单中添加复选框,并且name="remember-me" value="true"。
1. 修改登录页面

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="user/login" method="post"> 用户名:<input type="text" name="uname"> <br> 密码:<input type="text" name="pwd"> <br> 记住我:<input type="checkbox" name="remember-me" value="true"> <br> <input type="submit"> </form> </body> </html>
2. 添加配置类
@Configuration public class RememberMeConfig { @Autowired private DataSource dataSource; @Bean public PersistentTokenRepository getPersistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl=new JdbcTokenRepositoryImpl(); jdbcTokenRepositoryImpl.setDataSource(dataSource); // 自动建表,第一次启动时需要,第二次启动时注释掉 //jdbcTokenRepositoryImpl.setCreateTableOnStartup(true); return jdbcTokenRepositoryImpl; } }
3. 修改SecurityConfig配置类
@Configuration public class SecurityConfig { //注入UserDetailsService @Autowired private UserDetailsService userDetailsService; //滤器链的相关设置 @Bean //新添加,令牌库 public SecurityFilterChain filterChain(HttpSecurity http, PersistentTokenRepository tokenRepository) throws Exception { // 其它配置需要添加 ... //remember Me http.rememberMe() // .tokenValiditySeconds(120) //单位:秒。默认2周 // .rememberMeCookieDomain("/") //设置cookie的域 .tokenRepository(tokenRepository) .userDetailsService(userDetailsService); return http.build(); } //密码比较,加密器 @Bean public PasswordEncoder getPwdEncoder() { return new BCryptPasswordEncoder(); } }
九、访问控制(授权)
两种方式:基于资源、基于角色
1. 基于资源访问控制
1.1 实体类

@TableName(value ="menu") @Data public class Menu implements Serializable { /** * 菜单编号 */ @TableId(type = IdType.AUTO) private Long id; /** * 菜单名称 */ private String name; /** * 菜单地址 */ private String url; /** * 菜单权限标识 */ private String permission; @TableField(exist = false) private static final long serialVersionUID = 1L; }
1.2 Mapper

<resultMap id="BaseResultMap" type="Menu"> <id property="id" column="id" jdbcType="BIGINT"/> <result property="name" column="name" jdbcType="VARCHAR"/> <result property="url" column="url" jdbcType="VARCHAR"/> <result property="permission" column="permission" jdbcType="VARCHAR"/> </resultMap> <select id="selectByUserId" resultMap="BaseResultMap"> select m.id, m.name, url, permission from user u join role_user ru on u.id=ru.uid join role r on ru.rid=r.id join role_menu rm on rm.rid=r.id join menu m on rm.mid=m.id where u.id=#{userId}; </select>
1.3 修改UserServiceImpl

@Service public class UserServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private MenuMapper menuMapper; /** * @param username 前端登录时提交的用户名 * @return 用户的认证信息,要求必须为UserDetails接口的实现类 * @throws UsernameNotFoundException 用户名没有找到时的异常 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, username); User myUser = userMapper.selectOne(wrapper); if (myUser == null){ throw new UsernameNotFoundException("用户名不存在"); } //查询用户拥有的权限 List<Menu> menus = menuMapper.selectByUserId(myUser.getId()); ArrayList<GrantedAuthority> list = new ArrayList<>(); menus.forEach(menu -> { //获取的权限为null时,将null添加到SimpleGrantedAuthority会出现 //A granted authority textual representation is required异常信息 String permission = menu.getPermission(); if (permission != null && permission!=""){ list.add(new SimpleGrantedAuthority(permission)); } }); //参数一:用户名 参数二:密码 参数三:权限 /* * org.springframework.security.core.userdetails.User为UserDetails接口的实现类。 * 也可以自定义的User实现UserDetail接口提供对应的属性。 * */ org.springframework.security.core.userdetails.User user = new org.springframework.security.core.userdetails.User( myUser.getUsername(), myUser.getPassword(), list); return user; } }
1.4 修改配置类

//请求授权设置。 /* * antMatchers():对指定的请求url进行控制 * permitAll():允许访问 * anyRequest():任意一个请求url的控制 * authenticated():认证后便可以访问 * hasAuthority():存在指定的权限可以访问 * hasAnyRole(String ...):存在指定的任意一个权限可以访问 * */ http.authorizeRequests() .antMatchers("/login.html").permitAll() //login.html不需要被认证 .antMatchers("/stu/select").hasAuthority("stu:select") //需要有指定的权限 .antMatchers("/stu/insert").hasAuthority("stu:insert") //需要有指定的权限 .antMatchers("/stu/update").hasAuthority("stu:update") //需要有指定的权限 .antMatchers("/stu/del").hasAuthority("stu:del") //需要有指定的权限 .anyRequest().authenticated(); //其它任意的请求都必须被认证,必须认证(登录成功)后就可以直接访问
1.5 控制器

@RestController @RequestMapping("stu") public class StudentController { @RequestMapping("insert") public String insert(){ return "添加学生"; } @RequestMapping("update") public String update(){ return "修改学生"; } @RequestMapping("del") public String del(){ return "删除学生"; } @RequestMapping("select") public String select(){ return "查询学生"; } }
2. 基于角色访问控制
2.1 实体类

@TableName(value ="role") @Data public class Role implements Serializable { /** * 角色编号 */ @TableId(type = IdType.AUTO) private Long id; /** * 角色名称 */ private String name; @TableField(exist = false) private static final long serialVersionUID = 1L; }
2.2 Mapper

<resultMap id="BaseResultMap" type="com.xxx.pojo.Role"> <id property="id" column="id" jdbcType="BIGINT"/> <result property="name" column="name" jdbcType="VARCHAR"/> </resultMap> <select id="selectByUserId" resultMap="BaseResultMap"> select r.id, r.name from user u join role_user ru on u.id=ru.uid join role r on ru.rid=r.id where u.id=1; </select>
2.3 修改UserServiceImpl
官网要求:角色前必须添加 ROLE_ 前缀,角色才会生效。

@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private MenuMapper menuMapper; @Autowired private RoleMapper roleMapper; /** * @param username 前端登录时提交的用户名 * @return 用户的认证信息,要求必须为UserDetails接口的实现类 * @throws UsernameNotFoundException 用户名没有找到时的异常 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, username); User myUser = userMapper.selectOne(wrapper); if (myUser == null) { throw new UsernameNotFoundException("用户名不存在"); } /* * org.springframework.security.core.userdetails.User为UserDetails接口的实现类。 * 也可以自定义的User实现UserDetail接口提供对应的属性。 * */ //查询用户拥有的权限 List<Menu> menus = menuMapper.selectByUserId(myUser.getId()); ArrayList<GrantedAuthority> list = new ArrayList<>(); menus.forEach(menu -> { //获取的权限为null时,将null添加到SimpleGrantedAuthority会出现 //A granted authority textual representation is required异常信息 String permission = menu.getPermission(); if (permission != null && permission != "") { list.add(new SimpleGrantedAuthority(permission)); } }); //查询用户所有角色 List<Role> roles = roleMapper.selectByUserId(myUser.getId()); roles.forEach(role -> { list.add(new SimpleGrantedAuthority("ROLE_" + role.getName())); }); //参数一:用户名 参数二:密码 参数三:权限 org.springframework.security.core.userdetails.User user = new org.springframework.security.core.userdetails.User( myUser.getUsername(), myUser.getPassword(), list); return user; } }
2.4 修改配置类

//请求授权设置。 /* * antMatchers():对指定的请求url进行控制 * permitAll():允许访问 * anyRequest():任意一个请求url的控制 * authenticated():认证后便可以访问 * hasAuthority():存在指定的权限可以访问 * hasAnyRole(String ...):存在指定的任意一个权限可以访问 * hasRole():存在指定的角色可以访问 * hasAnyRole():存在指定的任意一个角色可以访问 * */ http.authorizeRequests() .antMatchers("/login.html").permitAll() //login.html不需要被认证 .antMatchers("/stu/select").hasAuthority("stu:select") //需要有指定的权限 .antMatchers("/stu/insert").hasAuthority("stu:insert") //需要有指定的权限 .antMatchers("/stu/update").hasRole("管理员") //需要有指定的角色 .antMatchers("/stu/del").hasRole("管理员") //需要有指定的角色 .anyRequest().authenticated(); //其它任意的请求都必须被认证,必须认证(登录成功)后就可以直接访问
十、基于注解的访问控制
1.2 @PostAuthorize
- 表示方法或类执行结束后判断权限,此注解很少被使用到。
2. 配置类
配置类中不再需要角色和权限的访问控制
3. 启动类
在启动类中使用@EnableGlobalMethodSecurity开启@PreAuthorize注解
@SpringBootApplication @EnableGlobalMethodSecurity(prePostEnabled = true) public class MyApp { public static void main(String [] args){ SpringApplication.run(MyApp.class,args); } }
4. 控制器

@RestController @RequestMapping("stu") public class StudentController { @RequestMapping("insert") @PreAuthorize("hasAuthority('stu:select')") public String insert(){ return "添加学生"; } @RequestMapping("update") @PreAuthorize("hasAuthority('stu:update')") public String update(){ return "修改学生"; } @RequestMapping("del") @PreAuthorize("hasRole('ROLE_管理员')") public String del(){ return "删除学生"; } @RequestMapping("select") @PreAuthorize("hasRole('ROLE_管理员')") public String select(){ return "查询学生"; } }
十一、自定义403处理
-
AuthenticationEntryPoint 该类用来统一处理 AuthenticationException (认证)异常
-
AccessDeniedHandler 该类用来统一处理 AccessDeniedException (访问拒绝)异常
@Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e)
throws IOException, ServletException { httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}"); out.flush(); } }
2. 修改配置类
配置类中重点添加异常处理器。设置访问受限后交给哪个对象进行处理。
//或者直接写实现类 MyaccessDeniedHandler @Autowired private AccessDeniedHandler accessDeniedHandler;
//异常处理,设置之后才会使用自定义的类处理异常 http.exceptionHandling() .accessDeniedHandler(accessDeniedHandler);
十二、Spring Security整合Thymeleaf
-
Spring Security可以在一些视图技术中控制显示效果。例如:JSP或Thymeleaf。
-
在非前后端分离且使用Spring Boot的项目中多使用Thymeleaf作为视图。
Thymeleaf对Spring Security的支持都放在thymeleaf-extras-springsecurityX中,
需要在项目中添加此jar包的依赖和thymeleaf的依赖。
<dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
html添加命名空间。
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
1. 获取属性
可以在html页面中通过 sec:authentication=""获取UsernamePasswordAuthenticationToken中所有getXXX的内容,包含父类中的getXXX的内容。
根据源码得出下面属性:
-
name:登录账号名称
-
principal:登录主体,在自定义登录逻辑中是UserDetails
-
credentials:凭证
-
authorities:权限和角色
-
details:实际上是WebAuthenticationDetails的实例。可以获取remoteAddress(客户端ip)和sessionId(当前sessionId)

<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 登录账号:<span sec:authentication="name">123</span><br/> 登录账号:<span sec:authentication="principal.username">456</span><br/> 凭证:<span sec:authentication="credentials">456</span><br/> 权限和角色:<span sec:authentication="authorities">456</span><br/> 客户端地址:<span sec:authentication="details.remoteAddress">456</span><br/> sessionId:<span sec:authentication="details.sessionId">456</span><br/> </body> </html>
2. 权限判断

通过权限判断: <button sec:authorize="hasAuthority('stu:insert')">新增</button> <button sec:authorize="hasAuthority('stu:delete')">删除</button> <button sec:authorize="hasAuthority('stu:update')">修改</button> <button sec:authorize="hasAuthority('stu:select')">查看</button> <br/> 通过角色判断: <button sec:authorize="hasRole('管理员')">新增</button> <button sec:authorize="hasRole('管理员')">删除</button> <button sec:authorize="hasRole('管理员')">修改</button> <button sec:authorize="hasRole('管理员')">查看</button>
十三、Spring Security中CSRF
-
刚开始学习Spring Security时,在配置类中一直存在这样一行代码:http.csrf().disable();如果没有这行代码导致用户无法被认证。这行代码的含义是:关闭csrf防护。
1. CSRF
CSRF英文全称叫做: cross-site request forgery,翻译过来叫做跨站请求伪造。spring security默认情况下是开启了csrf保护的。
CSRF 是致击者通过一些技术手段欺骗用户的浏览器,去访问一个用户曾经认证过的网站并执行恶意请求,例如发送邮件、发消息、甚至财产操作(如转账和购买商品)。由于客户端(浏览器)已经在该网站中认证过了,所以该网站会认为是真正用户在操作而执行请求(实际上并非用户的本意)。
2. Spring Security中CSRF
从Spring Security4开始CSRF防护默认开启。
默认会拦截请求,以防止CSRF攻击应用程序处理。
默认情况下会启用 CSRF 保护,SpringSecurity CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。
要求访问时携带参数名为_csrf值为token(token在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
注意,这里面不包括GET、HEAD、TRACE、OPTIONS请求,GET、HEAD、TRACE、OPTIONS请求还是会存在这种问题的。
3. Spring Security中CSRF原理
-
当服务器加载登录页面,先生成csrf对象,并放入作用域中,key为_csrf。
-
用户在提交登录表单时,会携带csrf的token。如果客户端的token和服务器的token匹配说明是自己的客户端,否则无法继续执行。
-
用户退出的时候,必须发起POST请求,且和登录时一样,携带csrf的令牌。
4. 实现
4.1 login.html
在项目resources下新建templates文件夹,并在文件夹中新建login.html页面。form表单中的第一行是必须存在的否则无法正常登录。
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action = "/login" method="post"> <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/> 用户名:<input type="text" name="username"/><br/> 密码:<input type="password" name="password"/><br/> <input type="submit" value="登录"/> </form> </body>
4.2 修改配置类
在配置类中注释掉CSRF防护失效。
// http.csrf().disable();
十四、 认证方式
1. 传统的session认证
1.1 原理流程
session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中,具体流程如下:
1.2 认证流程
-
用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session。
-
请求返回时将此 Session 的唯一标识信息
SessionID
返回给浏览器。 -
浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名。
-
当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会 从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
总结
根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。
1.3 特点
1、将 session 存储在服务器里面,当用户同时在线量比较多时,这些 session 会占据较多的内存,需要在服务端定期的去清理过期的 session。
2、当网站采用集群部署的时候,会遇到多台 web 服务器之间如何做 session 共享的问题
。因为 session 是由单个服务器创建的,但是处理用户请求
的服务器不一定是那个创建 session 的服务器,那么该服务器就无法拿到之前已经放入到 session 中的登录凭证之类的信息了。
3、当多个应用要共享 session 时,除了以上问题,还会遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用做好 cookie 跨域的处理。
4、sessionId 是存储在 cookie 中的,假如浏览器禁止 cookie 或不支持 cookie 。
5、移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token。
6、CSRF:因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
2. Token(令牌)
2.1 原理流程
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。
2.2 认证流程
1、客户端使用用户名跟密码请求登录。
2、服务端收到请求,去认证服务器验证用户名与密码。
3、验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端。
4、客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里。
5、客户端每次向服务端请求资源的时候需要带着服务端签发的 token。
6、服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据。
2.3 特点
1、如果用数据库来存储 token 会导致查询时间太长,可以选择放在内存当中。比如 redis 很适合你对 token 查询的需求。
3、token 可以避免 CSRF 攻击(因为不需要 cookie 了)。
4、移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token。
2.4 Token 和 Session 的区别
Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。
Token 是令牌,访问资源接口(API)时携带token。Token使服务端无状态化,不会存储会话信息。
3. JWT
JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。
我们在使用token的时候会发现,前端给我们传了token之后,我们还需要拿者这个token去数据库查询用户信息,并返回。这样数据库的操作肯定会影响一定的性能,jwt就是来解决这个问题。
3.1 原理流程
3.2 认证流程
-
客户端发起认证请求:用户通过登录表单将用户名和密码发送到后端的接口,这一过程通常是一个 HTTP POST 请求。
-
服务端生成令牌(Token):服务端在核对用户名和密码成功后,会将用户的 id 等其他信息作为 JWT Payload(负载),并签名生成一个 JWT (Token),形成的 JWT 是一个形如 xxx.yyy.zzz 的字符串。
-
前端保存令牌(Token):JWT 字符串作为登录成功的返回结果返回给前端,前端将返回的结果保存在本地浏览器的 localStorage 或 sessionStorage 中。
-
后续请求携带令牌(Token):后续用户每次请求服务端资源时,都需要将 JWT 放入 HTTP Header 的 Authorization 位(Bearer + Token),避免了 XSS(跨站脚本攻击)和 CSRF(跨站请求伪造)问题。
-
服务端拦截请求解析并验证令牌(Token):后端会拦截请求,检查请求头中是否携带令牌(Token),如果存在则进行解析并验证其有效性。例如,检查签名是否正确,检查 Token 是否过期,检查 Token 的接收方是否是自己等。
-
响应结果:验证通过后,后端使用 JWT 中包含的用户信息进行其他逻辑操作,返回相应结果。
3.3 特点
它最大的优点为当服务端拿到JWT之后,我们不需要向token样还需去查询数据库校验信息,因为JWT中就包含用户信息,所以减少一次数据的查询,
但这样做也会带来很明显的问题:
-
无法满足修改密码场景:
服务端拿到jwt是不会在去查询数据库的,所以就算你改了密码,服务端还是未知的。那么假设号被到了,修改密码之后,盗号者在原 jwt 有效期之内依旧可以继续访问系统,需要强制性的修改。
-
无法满足注销场景
传统的 session+cookie 方案用户点击注销,服务端清空 session 即可,因为状态保存在服务端。但 jwt 的方案就比较难办了,因为 jwt 是无状态的,服务端通过计算来校验有效性。没有存储起来,所以即使客户端删除了 jwt,但是该 jwt 还是在有效期内,只不过处于一个游离状态。
-
无法满足token续签场景
我们知道微信只要你每天使用是不需要重新登录的,因为有token续签,因为传统的 cookie 续签方案一般都是框架自带的,session 有效期 30 分钟,30 分钟内如果有访问session 有效期被刷新至 30 分钟。但是 jwt 本身的 payload 之中也有一个 exp 过期时间参数,来代表一个 jwt 的时效性,而 jwt 想延期这个 exp 就有点身不由己了,因为payload 是参与签名的,一旦过期时间被修改,整个 jwt 串就变了,jwt 的特性天然不支持续签。
3.4 Token 和 JWT 的区别
-
相同
-
都是访问资源的令牌
-
都是使服务端无状态化
-
都是只有验证成功后,客户端才能访问服务端上受保护的资源
-
-
区别
-
Token 服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
-
JWT 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。
-
十五、JWT结构
JWT由三个以 “·
” 为分隔符的部分组成:标头(Header)、载荷|负载(Payload)以及签名(Signature)。
header.payload.signature
xxxxx.yyyyy.zzzzz
1. header(标头)
标头(Header)主要包含两个信息:令牌类型(typ)和所使用的加密算法(alg)。例如:HS256 或者 RSA。
将这部分信息采用 JSON 格式存储,然后通过 Base64 编码处理,就构成了 JWT 的第一部分。
例如:
{ "alg": "HS256", "typ": "JWT" }
Base64 仅仅是一种编码方式,而非加密方式,其内容可以很容易地解码出来。
2. Payload(载荷)
载荷(Payload)部分包含了所要传递的数据,通常这些数据都是一些声明(claims),例如:用户身份信息、token 的生成时间、过期时间等。
载荷也需要进行 Base64 编码,形成 JWT 的第二部分。
例如:
{ "sub": "Arvin", "iat": 1489079981393, "exp": 1489684781 }
sub:主体用户
iss:jwt签发者
aud:接收jwt的一方
exp:jwt的过期时间
nbf:定义在什么时间之前,jwt是不可用的
iat:jwt的签发时间
jti:jwt唯一标识
-
敏感信息避免存储在 JWT 的有效载荷或标头中,除非它们被加密。
3. Signature(签名)
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:header (base64后的) ,payload (base64后的),secret(盐)
这个部分需要base64编码后的header和base64编码后的payload使用 .
连接组成的字符串,然后通过header中声明的加密方式加secret组合加密,就构成了jwt的第三部分。
String signature = HMACSHA256(encodedString, 'secret');
-
secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,
在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
十六、JWT使用
1. 使用JWT生成token
-
JWT依赖
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
@Test //获取token void test1() { // 创建有效载荷中的声明 //支持自定义 Map<String,Object> claims = new HashMap<>(); claims.put("sub", "Arvin"); // 主体用户 claims.put("id", 1); // 用户 ID claims.put("created", new Date()); // 创建时间 // 生成 Token String token = Jwts.builder() .setHeaderParam("typ", "JWT") // 设置headr(标头):Token 类型(默认是 JWT) .setHeaderParam("alg", "HS256") // 设置headr(标头):签名算法 (默认是 HS256) .setClaims(claims) // 设置载荷 .signWith(SignatureAlgorithm.HS256, "hags213#ad&*sdk") // 设置签名使用的密钥和签名算法 .compact(); System.out.println(token); } eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBcnZpbiIsImNyZWF0ZWQiOjE3MDA0MDQ0NjY1NzMsImlkIjoxfQ.uHSVfg9IU3NHLAdRh_ZsIJEi18D1GoNV1EqIzWbnpo4
2. 解析token
@Test //解析token void test2(){ Claims claims = Jwts.parser() // 解析 .setSigningKey("hags213#ad&*sdk") // 设置密钥(会自动推断算法) .parseClaimsJws("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBcnZpbiIsImNyZWF0ZWQiOjE3MDA0MDQ0NjY1NzMsImlkIjoxfQ.uHSVfg9IU3NHLAdRh_ZsIJEi18D1GoNV1EqIzWbnpo4") // 设置要解析的 Token .getBody();// 获取载荷 System.out.println("用户名:" + claims.get("sub")); System.out.println("创建时间:" + claims.get("created")); System.out.println("用户 ID:" + claims.get("id")); }
常见异常:
-
SignatureVerificationException:签名不一致异常
-
TokenExpiredException:令牌过期异常
-
AlgorithmMismatchException:签名算法不匹配异常
-
InvalidClaimException:失效的 payload 异常
3. JWT工具类

package com.xxx.util; /** * JWT工具类 */ public class JwtUtil { //有效期为 public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时 //设置秘钥明文 public static final String JWT_KEY = "xxx"; /** * 创建token * @param username 用户名 * @return */ public static String createJWT(String username) { JwtBuilder builder = getJwtBuilder(username, null, getUUID());// 设置过期时间 return builder.compact(); } /** * 解析token * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } public static String getUUID(){ String token = UUID.randomUUID().toString().replaceAll("-", ""); return token; } private static JwtBuilder getJwtBuilder(String username, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) //唯一的ID .setSubject(username) //用户名 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥 .setExpiration(expDate); //过期时间 } /** * 生成加密后的秘钥 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } }
4. 测试工具类
@Test void test3(){ String token = JwtUtil.createJWT("zs"); System.out.println(token); Claims claims = JwtUtil.parseJWT(token); System.out.println(claims.getSubject()); }
十七、SpringSecurity整合JWT
1. 引入相关依赖

<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.25</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <!-- ... -->
2. 配置文件

# 数据源配置 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/ss?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: root redis: database: 1 host: 192.168.93.128 port: 6379 password: 123456 mybatis-plus: type-aliases-package: com.xxx.pojo configuration: #mybatis-plus日志控制台输出 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: #关闭banner banner: false
3. 实体类

@TableName(value ="user") @Data @NoArgsConstructor @AllArgsConstructor public class User implements UserDetails { private Long id; private String username; private String password; @TableField(select = false) private Set<GrantedAuthority> authorities; @TableField(select = false) private boolean accountNonExpired; @TableField(select = false) private boolean accountNonLocked; @TableField(select = false) private boolean credentialsNonExpired; @TableField(select = false) private boolean enabled; public User(Long id, String username,String password, Set<GrantedAuthority> authorities) { this.id = id; this.password = password; this.username = username; this.authorities = authorities; this.accountNonExpired = true; this.accountNonLocked = true; this.credentialsNonExpired = true; this.enabled = true; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public boolean isAccountNonExpired() { return accountNonExpired; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; } }
4. 工具类
4.1 JWT工具类

/** * JWT工具类 */ public class JwtUtil { //有效期为 public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时 //设置秘钥明文 public static final String JWT_KEY = "xxx"; /** * 创建token * @param username 用户名 * @return */ public static String createJWT(String username) { JwtBuilder builder = getJwtBuilder(username, null, getUUID());// 设置过期时间 return builder.compact(); } /** * 解析token * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } public static String getUUID(){ String token = UUID.randomUUID().toString().replaceAll("-", ""); return token; } private static JwtBuilder getJwtBuilder(String username, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) //唯一的ID .setSubject(username) //用户名 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥 .setExpiration(expDate); //过期时间 } /** * 生成加密后的秘钥 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } }
4.2 响应工具类

@Data public class ResponseResult<T> { /** * 状态码 */ private Integer code; /** * 提示信息,如果有错误时,前端可以获取该字段进行提示 */ private String msg; /** * 查询到的结果数据, */ private T data; public ResponseResult(Integer code, String msg) { this.code = code; this.msg = msg; } public ResponseResult(Integer code, T data) { this.code = code; this.data = data; } public ResponseResult(Integer code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } }
5. 编写控制器类

@RestController @RequestMapping("user") public class UserController { @Autowired private UserService userService; @RequestMapping("login") public ResponseResult login(String username, String password) throws Exception { System.out.println(username); System.out.println(password); ResponseResult resultUtil = userService.login(username, password); return resultUtil; } @RequestMapping("show") public String show(){ return "show"; } }
6. 编写业务层

public interface UserService { ResponseResult login(String username, String password) throws Exception; } @Service public class UserServiceImpl implements UserService, UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private AuthenticationConfiguration authenticationConfiguration; @Autowired private RedisTemplate redisTemplate; @Override public ResponseResult login(String username, String password) throws Exception { // 将客户端接收的 username 和 password 封装成UsernamePasswordAuthenticationToken UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); //获取认证管理器 AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager(); //认证用户信息 Authentication authenticate = authenticationManager.authenticate(authenticationToken); //获取封装在Authentication对象内部的User对象 User user = (User) authenticate.getPrincipal(); //获取username String uname = user.getUsername(); //使用用户名创建令牌 String token = JwtUtil.createJWT(uname); //用户认证后信息存入redis redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new FastJsonRedisSerializer<>(Object.class)); redisTemplate.opsForValue().set(uname, user); //把token响应给前端 HashMap<String, String> map = new HashMap<>(); map.put("token", token); return new ResponseResult(200, "登陆成功", map); } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, username); User myUser = userMapper.selectOne(wrapper); if (myUser == null){ throw new UsernameNotFoundException("用户名不存在"); } /* * org.springframework.security.core.userdetails.User为UserDetails接口的实现类。 * 也可以自定义的User实现UserDetail接口提供对应的属性。 * */ //参数一:用户名 参数二:密码 参数三:权限(暂时不授权,创建个没有元素集合。不能指定null) Set<GrantedAuthority> set = new HashSet<>(); User user = new User(myUser.getId(), myUser.getUsername(), myUser.getPassword(), set); return user; } }
7. 编写mapper
-
启动器类配置@MapperScanner
public interface UserMapper extends BaseMapper<User> { }
8. 编写配置类

@Configuration public class SecurityConfige { //修改过滤器链配置 @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeRequests() .mvcMatchers("/user/login").permitAll() .anyRequest().authenticated(); //Spring Security不会创建HttpSession,也不会使用它来获取用户信息 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.csrf().disable(); return http.build(); } // 加密 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
9. 添加自定义过滤器

@Component @Slf4j public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisTemplate redisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //获取token String token = request.getHeader("token"); log.info("接收到的token:" + token); if (!StringUtils.hasText(token)) { //放行 log.info("请求中没有token"); filterChain.doFilter(request, response); return; } log.info("请求中存在token"); //解析token String uname; try { //获取token中载荷 Claims claims = JwtUtil.parseJWT(token); //获取载荷中用户名 uname = claims.getSubject(); log.info("token中解析的用户名:" + uname); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } //从redis中获取用户信息 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new FastJsonRedisSerializer<>(Object.class)); JSONObject loginUser = (JSONObject) redisTemplate.opsForValue().get(uname); log.info("redis中获取的用户信息:" + loginUser); User user = loginUser.toJavaObject(User.class); log.info("redis中的用户信息转换为user对象:" + user); if(Objects.isNull(user)){ throw new RuntimeException("用户未登录"); } System.out.println(token); //存入SecurityContextHolder //获取权限信息封装到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,null); SecurityContextHolder.getContext().setAuthentication(authenticationToken); //放行 filterChain.doFilter(request, response); } }
10. 修改配置类
// at: ⽤来某个 filter 替换过滤器链中哪个 filter http.addFilterAt(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
11. 总结
-
登录成功后:
-
将用户名为key,用户对象为value 存储到 redis中。
-
生成jwt令牌,通过响应对象带回到浏览器中,浏览器存储令牌。
-
-
登录后访问其它的资源时:
-
请求头中携带令牌
-
自定义的过滤器中获取令牌
-
解析令牌,获取到负载|载荷。(负载中存储了用户名)
-
根据用户名到redis中查询用户数据
-
有:登录过了,存储用户权限。访问资源
-
没有:没有登录,完成登录
-
-
-
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异