摘要:整合 Spring Security 和 JWT 实现用户的登录和授权功能,同时改造
Swagger-UI的配置使其可自动发送登录令牌。
目录
[TOC]
整合 Spring Security 权限
添加依赖
在 pom.xml 中
1 | |
Spring Boot 配置
在Spring Boot应用程序中,可以通过在application.properties文件或application.yml文件中配置Spring Security。
- 这里的配置表示启用基于HTTP基本身份验证的Spring Security,并指定了一个用户名为admin,密码为password的用户。
- 默认情况下,Spring Boot UserDetailsServiceAutoConfiguration 自动化配置类,会创建一个内存级别的 InMemoryUserDetailsManager Bean 对象,提供认证的用户信息。
1 | |
添加配置类
1 | |
自定义的 Spring Security 安全配置类 SecurityConfig,可以通过继承WebSecurityConfigurerAdapter类、重写方法,实现自定义的 Spring Security 的配置。
configure(AuthenticationManagerBuilder auth):用于配置UserDetailsService及PasswordEncoder;AuthenticationManager:UserDetailsService:SpringSecurity 定义的核心接口,用于根据用户名获取用户信息,需要自行实现;UserDetails:定义用于封装用户信息的类(主要是用户信息和权限),需要自行实现;PasswordEncoder:用于对密码进行编码及校验比对的接口,目前使用的是BCryptPasswordEncoder哈希加密算法;
configure(HttpSecurity httpSecurity):用于读取并配置需拦截的 URL 路径、白名单路径,是否允许跨域(的OPTIONS请求),配置权限 JWT 过滤器、权限拒绝处理器、抛出异常后的处理器、动态权限校验过滤器等;-
添加
JwtAuthenticationTokenFilter类:自定义 JWT 登录授权过滤器/拦截器,(在用户名和密码校验前)添加过滤器 ,如果请求中有 JWT 的 token 且验证有效,会自行根据token信息进行登录。-
会取出 token 中的用户名,创建
UsernamePasswordAuthenticationToken实例,并保存至安全上下文。 -
调用
UserDetailsService、UserDetails、PasswordEncoder验证、取出;SecurityContextHolder.getContext().getAuthentication() == nullSecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken);:将 Authentication 保存至安全上下文。
-
-
添加
RestfulAccessDeniedHandler类:自定义权限拒绝处理器,当用户没有访问权限时的处理器,用于返回JSON格式的处理结果;:当未登录或token失效(登录过期)时,返回JSON格式的结果。RestAuthenticationEntryPoint
-
添加
DynamicSecurityFilter类:动态权限校验过滤器,用于实现基于路径的动态权限过滤;DynamicAccessDecisionManager类:动态权限决策管理器,用于判断用户是否有访问权限;DynamicSecurityMetadataSource类:动态权限数据源,用于获取动态权限规则;- 实现
DynamicSecurityService动态权限相关业务接口:读取动态权限配置;加载资源ANT通配符和资源对应MAP;
- 实现

示例一
自定义 Spring Security 的配置,实现权限控制。
SecurityConfig
在 cn.iocoder.springboot.lab01.springsecurity.config 包下,创建 SecurityConfig 配置类,继承 WebSecurityConfigurerAdapter 抽象类,实现 Spring Security 在 Web 场景下的自定义配置。
- 可以通过重写 WebSecurityConfigurerAdapter 的方法,实现自定义的 Spring Security 的配置。
1 | |
重写 #configure(AuthenticationManagerBuilder auth) 方法
重写 #configure(AuthenticationManagerBuilder auth) 方法,实现 AuthenticationManager 认证管理器。
1 | |
<X>处,调用AuthenticationManagerBuilder#inMemoryAuthentication()方法,使用内存级别的 InMemoryUserDetailsManager Bean 对象,提供认证的用户信息。- Spring 内置了两种 UserDetailsManager 实现:
- InMemoryUserDetailsManager,和「2. 快速入门」是一样的。
- JdbcUserDetailsManager ,基于 JDBC的 JdbcUserDetailsManager 。
- 实际项目中,更多采用调用
AuthenticationManagerBuilder#userDetailsService(userDetailsService)方法,使用自定义实现的 UserDetailsService 实现类,更加灵活且自由的实现认证的用户信息的读取。
- Spring 内置了两种 UserDetailsManager 实现:
<Y>处,调用AbstractDaoAuthenticationConfigurer#passwordEncoder(passwordEncoder)方法,设置 PasswordEncoder 密码编码器。- 在这里,为了方便,我们使用 NoOpPasswordEncoder 。实际上,等于不使用 PasswordEncoder ,不配置的话会报错。
- 生产环境下,推荐使用 BCryptPasswordEncoder 。更多关于 PasswordEncoder 的内容,推荐阅读《该如何设计你的 PasswordEncoder?》文章。
<Z>处,配置了「admin/admin」和「normal/normal」两个用户,分别对应 ADMIN 和 NORMAL 角色。相比「2. 快速入门」来说,可以配置更多的用户。
重写 #configure(HttpSecurity http) 方法
然后,重写 #configure(HttpSecurity http) 方法,主要配置 URL 的权限控制。
1 | |
<X>处,调用HttpSecurity#authorizeRequests()方法,开始配置 URL 的权限控制。注意看艿艿配置的四个权限控制的配置。下面,是配置权限控制会使用到的方法:#(String... antPatterns)方法,配置匹配的 URL 地址,基于 Ant 风格路径表达式 ,可传入多个。- 【常用】
#permitAll()方法,所有用户可访问。 - 【常用】
#denyAll()方法,所有用户不可访问。 - 【常用】
#authenticated()方法,登录用户可访问。 #anonymous()方法,无需登录,即匿名用户可访问。#rememberMe()方法,通过 remember me 登录的用户可访问。#fullyAuthenticated()方法,非 remember me 登录的用户可访问。#hasIpAddress(String ipaddressExpression)方法,来自指定 IP 表达式的用户可访问。- 【常用】
#hasRole(String role)方法, 拥有指定角色的用户可访问。 - 【常用】
#hasAnyRole(String... roles)方法,拥有指定任一角色的用户可访问。 - 【常用】
#hasAuthority(String authority)方法,拥有指定权限(authority)的用户可访问。 - 【常用】
#hasAuthority(String... authorities)方法,拥有指定任一权限(authority)的用户可访问。 - 【最牛】
#access(String attribute)方法,当 Spring EL 表达式的执行结果为true时,可以访问。
<Y>处,调用HttpSecurity#formLogin()方法,设置 Form 表单登录。- 如果胖友想要自定义登录页面,可以通过
#loginPage(String loginPage)方法,来进行设置。不过这里我们希望像「2. 快速入门」一样,使用默认的登录界面,所以不进行设置。
- 如果胖友想要自定义登录页面,可以通过
<Z>处,调用HttpSecurity#logout()方法,配置退出相关。- 如果胖友想要自定义退出页面,可以通过
#logoutUrl(String logoutUrl)方法,来进行设置。不过这里我们希望像「2. 快速入门」一样,使用默认的退出界面,所以不进行设置。
- 如果胖友想要自定义退出页面,可以通过
示例二
使用 Spring Security 的注解,实现权限控制。
3.3.1 SecurityConfig
修改 SecurityConfig 配置类,增加 @EnableGlobalMethodSecurity 注解,开启对 Spring Security 注解的方法,进行权限验证。
1 | |
DemoController
-
@PermitAll注解,等价于#permitAll()方法,所有用户可访问。重要!!!因为在「3.2.1 SecurityConfig」中,配置了
.anyRequest().authenticated(),任何请求,访问的用户都需要经过认证。所以这里@PermitAll注解实际是不生效的。也就是说,Java Config 配置的权限,和注解配置的权限,两者是叠加的。
-
@PreAuthorize注解,等价于#access(String attribute)方法,,当 Spring EL 表达式的执行结果为 true 时,可以访问。
添加 JwtTokenUtil 工具类
用于生成、解析、验证
JwtToken的工具类;
相关方法说明:
generateToken(UserDetails userDetails):用于根据登录用户信息生成token;validateToken(String token, UserDetails userDetails):判断token是否还有效;getUserNameFromToken(String token):从token中获取登录用户的信息;
登录注册功能实现
如果没有自定义登录界面,所以默认会使用 DefaultLoginPageGeneratingFilter 类,生成上述界面。
实现登录注册:
- 添加用户数据库表、及
model类com.macro.mall.model.UmsAdmin; - 添加
com.macro.mall.bo.AdminUserDetails:Spring Security 需要的用户详情; - 添加
UmsAdminController类:实现用户登录、注册及获取权限的接口;为接口中的方法添加访问权限。 - 添加
UmsAdminService接口及其实现类:核心接口,用于根据用户名获取用户信息,需自行实现;UmsAdminCacheService:用户缓存操作Service实现类,存在RedisService中。

给PmsBrandController接口中的方法添加访问权限:
- 给查询接口添加
pms:brand:read权限 - 给修改接口添加
pms:brand:update权限 - 给删除接口添加
pms:brand:delete权限 - 给添加接口添加
pms:brand:create权限
1 | |
整合 Swagger3
通过修改配置实现调用接口自带Authorization头,这样就可以访问需要登录的接口了。
参考:
直接在 Swagger 中填入认证信息
认证方式二、直接在 Swagger 中填入认证信息,这样就不用从外部去获取 access_token 了。
- 主要是 SecurityScheme 不同。这里采用了 OAuthBuilder 来构建,构建时即得配置 token 的获取地址。仅限于 OAuth2 模式。
Swagger 配置实现自带 Authorization 头
认证方式一、修改 Swagger 的配置,实现调用接口自带 Authorization 头,通过 Authorize 按钮设置 Token,即可访问需登录的接口。
-
在
BaseSwaggerConfig中配置一个 Docket Bean 实例,配置映射路径和要扫描的接口的位置。securityContexts:用来配置有哪些请求需要携带 Token。
-
配置
config/SwaggerConfig继承此BaseSwaggerConfig类:Swagger API 文档相关配置,没有 .yml 配置; -
在
application.yml中给Swagger-UI安全路径白名单放行。1
2
3
4secure: ignored: urls: #安全路径白名单 - /swagger-ui/index.html
Security 整合 Swagger3 相关配置的代码:
1 | |
Security 自动配置类
Spring Security 自动配置类,主要用于相关组件的配置
SecurityAutoConfiguration:
- SecurityProperties
- AuthenticationEntryPoint:认证失败处理类 Bean
- AccessDeniedHandler:权限不够处理器 Bean
- PasswordEncoder:Spring Security 加密器
- TokenAuthenticationFilter:Token 认证过滤器 Bean
- SecurityFrameworkService:
注意,不能和 {@link YudaoWebSecurityConfigurerAdapter} 用一个,原因是会导致初始化报错。
- 参见 https://stackoverflow.com/questions/53847050/spring-boot-delegatebuilder-cannot-be-null-on-autowiring-authenticationmanager 文档。
Token 认证过滤器
TokenAuthenticationFilter,在 SecurityAutoConfiguration 中调用。
- 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
- 情况一,基于 header[login-user] 获得用户,例如说来自 Gateway 或者其它服务透传
- 情况二,基于 Token 获得用户。
- 注意,这里主要满足直接使用 Nginx 直接转发到 Spring Cloud 服务的场景。
1 | |
整合 Spring Session
Spring Session + Redis
使用 Redis 作为 Spring Session 的存储器,这也是生产环境下,主流的选择。
依赖
1 | |
配置文件
在 resources 目录下,创建 application.yaml 配置文件。
1 | |
SessionConfiguration
创建 SessionConfiguration 类,自定义 Spring Session Redis 的配置。
- 在类上,添加
@EnableRedisHttpSession注解,开启自动化配置 Spring Session 使用 Redis 作为数据源。该注解有如下属性:maxInactiveIntervalInSeconds属性,Session 不活跃后的过期时间,默认为 1800 秒。redisNamespace属性,在 Redis 的 key 的统一前缀,默认为"spring:session"。redisFlushMode属性,Redis 会话刷新模式(RedisFlushMode)。目前有两种,默认为RedisFlushMode.ON_SAVE。RedisFlushMode.ON_SAVE,在请求执行完成时,统一写入 Redis 存储。RedisFlushMode.IMMEDIATE,在每次修改 Session 时,立即写入 Redis 存储。
cleanupCron属性,清理 Redis Session 会话过期的任务执行 CRON 表达式,默认为"0 * * * * *"每分钟执行一次。- 虽然说,Redis 自带了 key 的过期,但是惰性删除策略,实际过期的 Session 还在 Redis 中占用内存。
- 所以,Spring Session 通过定时任务,删除 Redis 中过期的 Session ,尽快释放 Redis 的内存。
- 不了解 Redis 的删除过期 key 的策略的胖友,可以看看 《Redis 中删除过期 Key 的三种策略》 文章。
- 在
#springSessionDefaultRedisSerializer()方法,定义了一个 Bean 名字为"springSessionDefaultRedisSerializer"的 RedisSerializer Bean ,采用 JSON 序列化方式。因为默认情况下,采用 Java 自带的序列化方式 ,可读性很差,所以进行替换。
1 | |
Spring Session + MongoDB
整合 Spring Session + Spring Security
整合 OAuth2
整合 JWT
项目实战
在开源项目翻了一圈,找到一个相对合适项目 RuoYi-Vue 。主要以下几点原因:
- 基于 Spring Security 实现。
- 基于 RBAC 权限模型,并且支持动态的权限配置。
- 基于 Redis 服务,实现登录用户的信息缓存。
- 前后端分离。同时前端采用 Vue ,相对来说后端会 Vue 的比 React 的多。
SysMenu
菜单权限实体类。
-
menuType属性,定义了三种类型。其中,F代表按钮,是为了做页面中的功能级的权限。 -
perms属性,对应的权限标识字符串。一般格式为${大模块}:${小模块}:{操作}。示例如下:1
2
3
4
5
6
7用户查询:system:user:query 用户新增:system:user:add 用户修改:system:user:edit 用户删除:system:user:remove 用户导出:system:user:export 用户导入:system:user:import 重置密码:system:user:resetPwd- 对于前端来说,每个按钮在展示时,可以判断用户是否有该按钮的权限。如果没有,则进行隐藏。当然,前端在首次进入系统的时候,会请求一次权限列表到本地进行缓存。
- 对于后端来说,每个接口上会添加
@PreAuthorize("@ss.hasPermi('system:user:list')")注解。在请求接口时,会校验用户是否有该 URL 对应的权限。如果没有,则会抛出权限验证失败的异常。 - 一个
perms属性,可以对应多个权限标识,使用逗号分隔。例如说:"system:user:query,system:user:add"。
SecurityConfig
在 SecurityConfig 配置类,继承 WebSecurityConfigurerAdapter 抽象类,实现 Spring Security 在 Web 场景下的自定义配置。
1 | |
- 涉及到的配置方法较多。
#configure(AuthenticationManagerBuilder auth)
重写 #configure(AuthenticationManagerBuilder auth) 方法,实现 AuthenticationManager 认证管理器。
1 | |
<X>处,调用AuthenticationManagerBuilder#userDetailsService(userDetailsService)方法,使用自定义实现的 UserDetailsService 实现类,更加灵活且自由的实现认证的用户信息的读取。在「7.3.1 加载用户信息」中,我们会看到 RuoYi-Vue 对 UserDetailsService 的自定义实现类。<Y>处,调用AbstractDaoAuthenticationConfigurer#passwordEncoder(passwordEncoder)方法,设置 PasswordEncoder 密码编码器。这里,就使用了 bCryptPasswordEncoder 强散列哈希加密实现。
#configure(HttpSecurity httpSecurity)
重写 #configure(HttpSecurity httpSecurity) 方法,主要配置 URL 的权限控制。
1 | |
比较长,选择重点的来看。
<X>处,设置认证失败时的处理器为unauthorizedHandler。详细解析,见「7.6.1 AuthenticationEntryPointImpl」。<Y>处,设置用于登录的/login接口,允许匿名访问。这样,后续我们就可以使用自定义的登录接口。详细解析,见「7.3 登录 API 接口」。<Z>处,设置登出成功的处理器为logoutSuccessHandler。详细解析,见「7.6.3 LogoutSuccessHandlerImpl」。<P>处,添加 JWT 认证过滤器authenticationTokenFilter,用于用户使用用户名与密码登录完成后,后续请求基于 JWT 来认证。 详细解析,见「7.4 JwtAuthenticationTokenFilter」。
#authenticationManagerBean
重写 #authenticationManagerBean 方法,解决无法直接注入 AuthenticationManager 的问题。
- 在方法上,额外添加了
@Bean注解,保证创建出 AuthenticationManager Bean 。
1 | |
JwtAuthenticationTokenFilter
在 JwtAuthenticationTokenFilter 中,继承 OncePerRequestFilter 过滤器,实现了基于 Token 的认证。
权限验证
在「3. 进阶使用」中,我们看到可以通过 Spring Security 提供的 @PreAuthorize 注解,实现基于 Spring EL 表达式的执行结果为 true 时,可以访问,从而实现灵活的权限校验。
在 RuoYi-Vue 中,通过 @PreAuthorize 注解的特性,使用其 PermissionService 提供的权限验证的方法。
- 请求
/system/dict/data/list接口,会调用 PermissionService 的#hasPermi(String permission)方法,校验用户是否有指定的权限。 - 为什么这里会有一个
@ss呢?在 Spring EL 表达式中,调用指定 Bean 名字的方法时,使用@+ Bean 的名字。在 RuoYi-Vue 中,声明 PermissionService 的 Bean 名字为ss。
1 | |
判断是否有权限
在 PermissionService 中,定义了 #hasPermi(String permission) 方法,判断当前用户是否有指定的权限。
判断是否有角色
在 PermissionService 中,定义了 #hasRole(String role) 方法,判断当前用户是否有指定的角色。
各种处理器
在 Ruoyi-Vue 中,提供了各种处理器,处理各种情况,所以我们汇总在「7.6 各种处理器」 中,一起来瞅瞅。
AuthenticationEntryPointImpl
在 AuthenticationEntryPointImpl 中,实现 Spring Security AuthenticationEntryPoint 接口,处理认失败的 AuthenticationException 异常。
GlobalExceptionHandler
在 GlobalExceptionHandler 中,定义了对 Spring Security 的异常处理。
LogoutSuccessHandlerImpl
在 LogoutSuccessHandlerImpl 中,实现 Spring Security LogoutSuccessHandler 接口,自定义退出的处理,主动删除 LoginUser 在 Redis 中的缓存。
权限管理
如下的 Controller ,提供了 RuoYi-Vue 的权限管理功能,比较简单,胖友自己去瞅瞅即可。
- 用户管理 SysUserController :用户是系统操作者,该功能主要完成系统用户配置。
- 角色管理 SysRoleController :角色菜单权限分配、设置角色按机构进行数据范围权限划分。
- 菜单管理 SysMenuController :配置系统菜单,操作权限,按钮权限标识等。
权限
RBAC 权限模型
系统采用 RBAC 权限模型,全称是 Role-Based Access Control 基于角色的访问控制。
- 简单来说,每个用户拥有若干角色,每个角色拥有若干个菜单,菜单中存在菜单权限、按钮权限。
-
这样,就形成了 “用户<->角色<->菜单” 的授权模型。
- 在这种模型中,用户与角色、角色与菜单之间构成了多对多的关系。
Token 认证
Token 存储在数据库中,对应 system_oauth2_access_token 访问令牌表的 id 字段。
- 考虑到访问的性能,缓存在 Redis 的
oauth2_access_token:%s键中。
疑问:为什么不使用 JWT(JSON Web Token)?
- JWT 是无状态的,无法实现 Token 的作废,例如说用户登出系统、修改密码等场景。
默认配置下,Token 有效期为 30 天,可通过 system_oauth2_client 表中 client_id = default 的记录进行自定义:

- 修改
access_token_validity_seconds字段,设置访问令牌的过期时间,默认 1800 秒 = 30 分钟 - 修改
refresh_token_validity_seconds字段,设置刷新令牌的过期时间,默认 2592000 秒 = 30 天
② 前端调用其它接口,需要在请求头带上 Token 进行访问。
请求头格式如下:
1 | |
- 具体的代码实现,可见 TokenAuthenticationFilter 过滤器。
Token 的模拟机制
考虑到使用 Postman、Swagger 调试接口方便,提供了 Token 的模拟机制。
请求头格式如下:
1 | |
其中 "test" 可自定义,配置项如下:
1 | |
@PreAuthorize 权限注解
@PreAuthorize (opens new window)是 Spring Security 内置的前置权限注解,添加在接口方法上,声明需要的权限,实现访问权限的控制。
基于【权限标识】的权限控制
权限标识:对应 system_menu 权限相关表的 permission 字段,推荐格式为 ${系统}:${模块}:${操作},例如说 system:admin:add 标识 system 服务的添加管理员。
使用示例如下:
1 | |
基于【角色标识】的权限控制
权限标识:对应 system_role 表的 code 字段, 例如说 super_admin 超级管理员、tenant_admin 租户管理员。
使用示例如下:
1 | |
实现原理是什么?
@PreAuthorize("@ss.hasPermission('system:user:list')")表示调用 Bean 名字为ss的#hasPermission(...)方法,方法参数为"system:user:list"字符串。ss对应的 Bean 是 PermissionServiceImpl类,所以只需要去看该方法的实现代码。- 当
@PreAuthorize注解里的 Spring EL 表达式返回false时,表示没有权限。
自定义权限配置
默认配置下,所有接口都需要登录后才能访问,不限于管理后台的 /admin-api/** 所有 API 接口、用户 App 的 /app-api/** 所有 API 接口。
如下想要自定义权限配置,设置定义 API 接口可以匿名(不登录)进行访问,可以通过下面三种方式:
自定义 AuthorizeRequestsCustomizer 实现
每个 Maven Module 可以实现自定义的 AuthorizeRequestsCustomizer (opens new window)Bean,额外定义每个 Module 的 API 接口的访问规则。例如说 yudao-module-infra 模块的 SecurityConfiguration (opens new window)类。
1 | |
友情提示
permitAll()方法:所有用户可以任意访问,包括带上 Token 访问anonymous()方法:匿名用户可以任意访问,带上 Token 访问会报错
如果你对 Spring Security 了解不多,可以阅读艿艿写的 《芋道 Spring Boot 安全框架 Spring Security 入门 》 (opens new window)文章。
@PermitAll 注解
在 API 接口上添加 @PermitAll (opens new window)注解,示例如下:
1 | |
配置文件
在 application.yaml 配置文件,设置yudao.security.permit-all-urls 配置项。
1 | |