Spring Security 简介
Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。
正如你可能知道的关于安全方面的两个主要区域是“认证 ”和“授权 ”(或者访问控制),一般来说,Web 应用的安全性包括 用户认证(Authentication)和用户授权(Authorization) 两个部分,这两点也是 Spring Security 重要核心功能。
用户认证 :指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录
用户授权 :指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。
历史
“Spring Security 开始于 2003 年年底,““spring 的 acegi 安全系统”。 起因是 Spring 开发者邮件列表中的一个问题,有人提问是否考虑提供一个基于 spring 的安全实现。
Spring Security 以“The Acegi Secutity System for Spring” 的名字始于 2013 年晚些时候。一个问题提交到 Spring 开发者的邮件列表,询问是否已经有考虑一个机遇 Spring 的安全性社区实现。那时候 Spring 的社区相对较小(相对现在)。实际上 Spring 自己在2013 年只是一个存在于 ScourseForge 的项目,这个问题的回答是一个值得研究的领域,虽然目前时间的缺乏组织了我们对它的探索。
考虑到这一点,一个简单的安全实现建成但是并没有发布。几周后,Spring 社区的其他成员询问了安全性,这次这个代码被发送给他们。其他几个请求也跟随而来。到 2014 年一月大约有 20 万人使用了这个代码。这些创业者的人提出一个 SourceForge 项目加入是为了,这是在 2004 三月正式成立。
在早些时候,这个项目没有任何自己的验证模块,身份验证过程依赖于容器管理的安全性和 Acegi 安全性。而不是专注于授权。开始的时候这很适合,但是越来越多的用户请求额外的容器支持。容器特定的认证领域接口的基本限制变得清晰。还有一个相关的问题增加新的容器的路径,这是最终用户的困惑和错误配置的常见问题。
Acegi 安全特定的认证服务介绍。大约一年后,Acegi 安全正式成为了 Spring 框架的子项目。1.0.0 最终版本是出版于 2006 -在超过两年半的大量生产的软件项目和数以百计的改进和积极利用社区的贡献。Acegi 安全 2007 年底正式成为了 Spring 组合项目,更名为"Spring Security"。
对比
Spring Security 特点:
和 Spring 无缝整合。
全面的权限控制。
专门为 Web 开发而设计。
旧版本不能脱离Web 环境使用。新版本对整个框架进行了分层抽取,分成了核心模块和Web 模块。单独引入核心模块就可以脱离Web 环境。
重量级 。
Shiro 特点:
Apache 旗下的轻量级权限控制框架。
轻量级 。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。
通用性:
好处:不局限于Web 环境,可以脱离Web 环境使用。
缺陷:在Web 环境下一些特定的需求需要手动编写代码定制。
Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。
相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。
自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 Spring Security。
模块划分
基本概念
什么是认证
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。 认证 :用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信 息,身份合法方可继续访问,不合法则拒绝访问。
常见的用户身份认证方式有:用户名密码登录,二维码登录,手 机短信登录,指纹认证等方式。
基于 Session 的认证方式:
基于 Token 的认证方式:
什么是授权
还拿微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等。没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即功能资源,用户拥有发红包功能的权限才可以正常使用发送红包功能,拥有发朋友圈功能的权限才可以使用发朋友圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。
认证是为了保证用户身份的合法性 ,授权则是为了更细粒度地对隐私数据进行划分 ,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。
授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
授权的数据模型
初始版本:资源表和权限表是分开的
资源表 (资源id、资源名称、访问地址、…)
权限表 (权限id、权限标识、权限名称、资源id、…)
但通常企业开发中将资源和权限表合并为一张权限表 (权限id、权限标识、权限名称、资源名称、资源访问地址、…)
修改后数据模型之间的关系如下图:
RBAC
如何实现授权?业界通常基于RBAC实现授权。
基于角色的访问控制
RBAC 基于角色的访问控制(Role-Based Access Control)是按角色 进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等,访问控制流程如下:
授权代码可表示如下:
1 2 3 if (主体.hasRole("总经理角色id" )) { 查询工资 }
而一旦查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是 总经理或部门经理”,修改代码如下:
1 2 3 if (主体.hasRole("总经理角色id" ) || 主体.hasRole("部门经理角色id" )) { 查询工资 }
根据例子发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差 。
基于资源的访问控制
RBAC 基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,比如:用户必须具有查询工资权限才可以查询员工工资信息等,访问控制流程如下:
授权代码可以表示为:
1 2 3 if (主体.hasPermission("查询工资权限标识" )) { 查询工资 }
优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强 。
Spring Security 工作原理
更多详细介绍见文档 https://blog.yuyunzhao.cn/documents/SpringSecurity.pdf
Spring Security 所解决的问题就是安全访问控制 ,而安全访问控制功能其实就是对所有进入系统的请求进行拦截 , 校验每个请求是否能够访问它所期望的资源。可以通过 Filter 或 AOP 等技术来实现,Spring Security 对 Web 资源的保护是通过 Filter 实现的,所以从 Filter 来入手,逐步深入 Spring Security 原理。
Spring Security 本质是一个过滤器链 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter org.springframework.security.web.context.SecurityContextPersistenceFilter org.springframework.security.web.header.HeaderWriterFilter org.springframework.security.web.csrf.CsrfFilter org.springframework.security.web.authentication.logout.LogoutFilter org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter org.springframework.security.web.savedrequest.RequestCacheAwareFilter org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter org.springframework.security.web.authentication.AnonymousAuthenticationFilter org.springframework.security.web.session.SessionManagementFilter org.springframework.security.web.access.ExceptionTranslationFilter org.springframework.security.web.access.intercept.FilterSecurityInterceptor
当初始化 Spring Security 时,会创建一个名为 SpringSecurityFilterChain
的 Filter 过滤器,类型为 org.springframework.security.web.FilterChainProxy
,它实现了 javax.servlet.Filter
,因此外部的请求会经过此类,下图是 Spring Security 过滤器链结构图:
FilterChainProxy
是一个代理,真正起作用的是 FilterChainProxy
中 SecurityFilterChain
所包含的各个 Filter,同时这些 Filter 作为 Bean 被 Spring 管理,它们是 Spring Security 核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了**认证管理器(AuthenticationManager)和 决策管理器 (AccessDecisionManager)**进行处理,下图是 FilterChainProxy
相关类的 UML 图示:
Spring Security 功能的实现主要是由一系列过滤器链相互配合完成:
SecurityContextPersistenceFilter
这个Filter是整个拦截过程的入口 和出口 (也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository
中获取 SecurityContext
,然后把它设置给 SecurityContextHolder
。在请求完成后将 SecurityContextHolder
持有的 SecurityContext
再保存到配置好 的 SecurityContextRepository
,同时清除 SecurityContextHolder
所持有的 SecurityContext
;
UsernamePasswordAuthenticationFilter
用于处理来自表单 提交的认证(认证过滤器)。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler
和 AuthenticationFailureHandler
,这些都可以根据需求做相关改变;
FilterSecurityInterceptor
是用于保护 web 资源 的(权限过滤器),使用 AccessDecisionManager
对当前用户进行授权访问:根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器 ExceptionTranslationFilter
捕获
ExceptionTranslationFilter
能够捕获来自 FilterChain
所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException
和 AccessDeniedException
,其它的异常它会继续抛出。
只有当前过滤器通过,才能进入下一个过滤器,任何一个不通过都会抛出异常,被 ExceptionTranslationFilter
捕获后返回给客户端 403 Forbidden
。
认证流程
认证流程主要使用 UsernamePasswordAuthenticationFilter
对提交的表单 进行认证,判断前端传来的用户名和密码是否匹配。流程图:
其中,UsernamePasswordAuthenticationFilter
本身不进行验证,而是将请求信息封装成 UsernamePasswordAuthenticationToken
对象后调用 AuthenticationManager
的 authenticate()
方法进行认证。其将委托 DaoAuthenticationProvider
进行验证,进一步调用 UserDetailsService
的 loadUserByUsername()
方法获取到当前用户名的密码、权限等信息。最后填充这些信息到 Authentication
对象中存入安全上下文中。
详细流程:
用户提交用户名、密码被 SecurityFilterChain
中的 UsernamePasswordAuthenticationFilter
过滤器获取到, 封装为请求Authentication
,通常情况下是 UsernamePasswordAuthenticationToken
这个实现类。
然后过滤器将 Authentication
提交至认证管理器(AuthenticationManager
)进行认证
认证成功后, AuthenticationManager
身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication
实例。
SecurityContextHolder
安全上下文容器将第3步填充了信息的 Authentication
,通过 SecurityContextHolder.getContext().setAuthentication(…)
方法,设置到其中。
可以看出 AuthenticationManager
接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为 ProviderManager
。而 Spring Security 支持多种认证方式,因此 ProviderManager
维护着一个 List
列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider
完成的。咱们知道 web 表单的对应的 AuthenticationProvider
实现类为 DaoAuthenticationProvider
,它的内部又维护着一个 UserDetailsService
负责 UserDetails
的获取。最终 AuthenticationProvider
将 UserDetails
填充至 Authentication
。
授权流程
Spring Security 可以通过 http.authorizeRequests()
对 web 请求进行授权保护。Spring Security 使用标准 Filter 建立了对 web 请求的拦截,最终实现对资源的授权访问。根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器 ExceptionTranslationFilter
捕获:
分析授权流程:
拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain
中的 FilterSecurityInterceptor
的子类拦截。
获取资源访问策略,FilterSecurityInterceptor
会从 SecurityMetadataSource
的子类 DefaultFilterInvocationSecurityMetadataSource
获取要访问当前资源所需要的权限 Collection
。SecurityMetadataSource
其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则,读取访问策略如:
1 2 3 4 5 http .authorizeRequests() .antMatchers("/r/r1" ).hasAuthority("p1" ) .antMatchers("/r/r2" ).hasAuthority("p2" ) ...
最后,FilterSecurityInterceptor
会调用 AccessDecisionManager
进行授权决策,若决策通过,则允许访问资源,否则将禁止访问(抛出异常)。
所有过滤器
WebAsyncManagerIntegrationFilter
:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
SecurityContextPersistenceFilter
:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder 中的信息清除,例如在 Session 中维护一个用户的安全信息就是这个过滤器处理的。
HeaderWriterFilter
:用于将头信息加入响应中。
CsrfFilter
:用于处理跨站请求伪造。
LogoutFilter
:用于处理退出登录。
UsernamePasswordAuthenticationFilter
:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的 usernameParameter 和 passwordParameter 两个参数的值进行修改**(可自定义该过滤器重写方法,实现从数据库中读取用户名和密码等信息)**
DefaultLoginPageGeneratingFilter
:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
BasicAuthenticationFilter
:检测和处理 http basic 认证**(可自定义该过滤器重写方法,从 Redis 中读取认证后用户的权限列表信息)**
RequestCacheAwareFilter
:用来处理请求的缓存。
SecurityContextHolderAwareRequestFilter
:主要是包装请求对象 request。
AnonymousAuthenticationFilter
:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
SessionManagementFilter
:管理 session 的过滤器
ExceptionTranslationFilter
:处理 AccessDeniedException 和 AuthenticationException 异常。
FilterSecurityInterceptor
:可以看做过滤器链的出口。
RememberMeAuthenticationFilter
:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
总结
主要流程:
Tomcat 收到浏览器发来的请求后使用上述过滤器逐个过滤请求
先经过认证过滤器 ,判断前端传来的用户名和密码是否匹配
若匹配,则经过授权过滤器 ,将用户的权限列表信息存储到 Spring Security 上下文对象中(方便后面 Controller 层获取到用户权限信息)
Spring Security 的所有过滤器都放行后才会进入到 Spring MVC 的 DispatcherServlet
单体 Web 应用权限方案
配置案例
导入 Maven 依赖 spring-boot-starter-security
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.0.5</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > </dependencies >
自定义登录用户细节 类(实现 UserDetailsService
接口),该类的 loadUserByUsername
方法将被 DaoAuthenticationProvider
调用,从数据库中查询指定用户名的密码与权限,并返回 UserDetails
对象给 DaoAuthenticationProvider
,验证前端传来的密码是否和数据库中的匹配:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Service("userDetailsService") public class MyUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername (String s) throws UsernameNotFoundException { QueryWrapper<User> wrapper = new QueryWrapper(); wrapper.eq("username" , s); User user = userMapper.selectOne(wrapper); if (user == null ) { throw new UsernameNotFoundException("用户名不存在!" ); } List<String> permissions = userMapper.findPermissionsByUserId(user.getId()); String[] perarray = new String[permissions.size()]; permissions.toArray(perarray); UserDetails userDetails = User. withUsername(user.getFullname()). password(user.getPassword()). authorities(perarray). build(); return userDetails; } }
该方法被调用的时机:
Spring Security 核心配置类 SecurityConfig
。负责配置安全拦截机制 等信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 @Configuration @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository () { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(password()); } @Bean PasswordEncoder password () { return new BCryptPasswordEncoder(); } @Override protected void configure (HttpSecurity http) throws Exception { http.logout().logoutUrl("/logout" ). logoutSuccessUrl("/test/hello" ).permitAll(); http.exceptionHandling().accessDeniedPage("/unauth.html" ); http.formLogin() .loginPage("/on.html" ) .loginProcessingUrl("/user/login" ) .defaultSuccessUrl("/success.html" ).permitAll() .failureUrl("/unauth.html" ) .and() .authorizeRequests() .antMatchers("/" , "/test/hello" , "/user/login" ).permitAll() .antMatchers("/test/index" ).hasAuthority("admins" ) .anyRequest().authenticated() .and() .rememberMe() .tokenRepository(persistentTokenRepository()) .userDetailsService(userDetailsService) .tokenValiditySeconds(60 ); } }
自定义前端登录页面:
1 2 3 4 5 6 7 <form action ="/logn" method ="post" > <input type ="hidden" name ="${_csrf.parameterName}" value ="${_csrf.token}" /> 用户名:<input type ="text" name ="username" /> <br /> 密码:<input type ="password" name ="password" /> <br /> 记住我:<input type ="checkbox" name ="remember-me" title ="记住密码" /> <br /> <input type ="submit" value ="提交" /> </form >
注意 :页面提交方式必须 POST 请求,并且用户名密码必须为 username
,password
。原因:执行登录的过程中会经过一个过滤器 UsernamePasswordAuthenticationFilter
,其默认配置的用户名和密码就是该值。如果想修改可以调用 http
对象的 usernameParameter()
和 passwordParameter()
方法。若想开启免登陆功能,必须添加一个复选框 remember-me
Spring Security 默认开启了 CSRF 防护 ,此时除了 GET 请求之外的请求都无法正常访问。除非在表单里添加一个隐藏域 ${_csrf.parameterName}
,每次提交时携带一个 token。Spring Security 会在用户登录时保存该 token,之后的非 GET 请求都会校验该 token,一致时才能允许放行请求。
Controller 层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @RestController @RequestMapping("/test") public class TestController { @GetMapping("hello") public String hello () { return "hello security" ; } @GetMapping("index") public String index () { return "hello index" ; } @GetMapping("update") @PostAuthorize("hasAnyAuthority('admins')") public String update () { System.out.println("update......" ); return "hello update" ; } @GetMapping("getAll") @PostAuthorize("hasAnyAuthority('admins')") @PostFilter("filterObject.username == 'admin1'") public List<Users> getAllUser () { ArrayList<Users> list = new ArrayList<>(); list.add(new Users(11 ,"admin1" ,"6666" )); list.add(new Users(21 ,"admin2" ,"888" )); System.out.println(list); return list; } }
配置细节
关于配置的更多细节见文档 https://blog.yuyunzhao.cn/documents/SpringSecurity.pdf
分布式应用权限方案
创建统一认证服务 UAA,负责进行统一认证与授权。经过该服务授权后,才会进入到具体的微服务。
统一认证服务 UAA
核心配置类
导入 Maven 依赖:
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > </dependency >
Spring Security 核心配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter { private TokenManager tokenManager; private RedisTemplate redisTemplate; private UserDetailsService userDetailsService; @Autowired public TokenWebSecurityConfig (UserDetailsService userDetailsService, TokenManager tokenManager, RedisTemplate redisTemplate) { this .userDetailsService = userDetailsService; this .tokenManager = tokenManager; this .redisTemplate = redisTemplate; } @Override protected void configure (HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(new UnauthEntryPoint()) .and() .csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and() .logout().logoutUrl("/admin/acl/index/logout" ) .addLogoutHandler(new TokenLogoutHandler(tokenManager, redisTemplate)) .and() .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate)) .addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate)) .httpBasic(); } @Override public void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(new BCryptPasswordEncoder()); } @Override public void configure (WebSecurity web) throws Exception { web.ignoring().antMatchers("/api/**" ); } }
自定义过滤器
共需要自定义两个过滤器:
令牌认证 过滤器 TokenLoginFilter
:实现自定义登录认证,在认证成功后保存用户名 和权限列表 到 Redis 中,方便后续授权过滤器获取到该用户的权限列表
令牌授权 过滤器 TokenAuthFilter
:从 Redis 中读取该用户的权限列表,存储到上下文 SecurityContextHolder.getContext()
中
自定义认证过滤器 TokenLoginFilter
,实现自定义登录认证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate redisTemplate; private AuthenticationManager authenticationManager; public TokenLoginFilter (AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) { this .authenticationManager = authenticationManager; this .tokenManager = tokenManager; this .redisTemplate = redisTemplate; this .setPostOnly(false ); this .setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login" ,"POST" )); } @Override public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { User user = new ObjectMapper().readValue(request.getInputStream(), User.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>())); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(); } } @Override protected void successfulAuthentication (HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityUser user = (SecurityUser)authResult.getPrincipal(); String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername()); redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList()); ResponseUtil.out(response, R.ok().data("token" ,token)); } protected void unsuccessfulAuthentication (HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { ResponseUtil.out(response, R.error()); } }
自定义登录用户细节 类(实现 UserDetailsService
接口),该类的 loadUserByUsername
方法将被上面自定义的认证过滤器 TokenLoginFilter
调用,从数据库中查询指定用户名的密码与权限,并返回 UserDetails
对象给 DaoAuthenticationProvider
,验证前端传来的密码是否和数据库中的匹配:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Service("userDetailsService") public class MyUserDetailsService implements UserDetailsService { @Autowired @Autowired private UserService userService; @Autowired private PermissionService permissionService; @Override public UserDetails loadUserByUsername (String s) throws UsernameNotFoundException { User user = userService.selectByUsername(username); if (user == null ) { throw new UsernameNotFoundException("用户名不存在!" ); } List<String> permionssions = permissionService.selectPermissionValueByUserId(user.getId()); UserDetails userDetails = User. withUsername(user.getFullname()). password(user.getPassword()). authorities(permionssions). build(); return userDetails; } }
该方法被调用的时机:
自定义令牌授权过滤器 TokenAuthFilter
:从 Redis 中读取该用户的权限列表,存储到上下文 SecurityContextHolder.getContext()
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class TokenAuthFilter extends BasicAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenAuthFilter (AuthenticationManager authenticationManager,TokenManager tokenManager,RedisTemplate redisTemplate) { super (authenticationManager); this .tokenManager = tokenManager; this .redisTemplate = redisTemplate; } @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { UsernamePasswordAuthenticationToken authRequest = getAuthentication(request); if (authRequest != null ) { SecurityContextHolder.getContext().setAuthentication(authRequest); } chain.doFilter(request,response); } private UsernamePasswordAuthenticationToken getAuthentication (HttpServletRequest request) { String token = request.getHeader("token" ); if (token != null ) { String username = tokenManager.getUserInfoFromToken(token); List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username); Collection<GrantedAuthority> authority = new ArrayList<>(); for (String permissionValue : permissionValueList) { SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue); authority.add(auth); } return new UsernamePasswordAuthenticationToken(username, token, authority); } return null ; } }
这样存储在 SecurityContextHolder.getContext()
中用户信息就可以在 Controller 层获取到:
1 2 3 4 5 6 7 @GetMapping("menu") public R getMenu () { String username = SecurityContextHolder.getContext().getAuthentication().getName(); List<JSONObject> permissionList = indexService.getMenu(username); return R.ok().data("permissionList" , permissionList); }
自定义处理器
自定义 JWT 生成器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Component public class TokenManager { public static final long EXPIRE = 1000 * 60 * 60 * 24 ; public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO" ; public String createToken (String username) { String token = Jwts.builder() .setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + tokenEcpiration)) .signWith(SignatureAlgorithm.HS512, tokenSignKey) .compressWith(CompressionCodecs.GZIP) .compact(); return token; } public String getUserInfoFromToken (String token) { String userinfo = Jwts.parser() .setSigningKey(tokenSignKey) .parseClaimsJws(token) .getBody() .getSubject(); return userinfo; } public void removeToken (String token) { } }
自定义 logout
处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class TokenLogoutHandler implements LogoutHandler { private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenLogoutHandler (TokenManager tokenManager,RedisTemplate redisTemplate) { this .tokenManager = tokenManager; this .redisTemplate = redisTemplate; } @Override public void logout (HttpServletRequest request, HttpServletResponse response, Authentication authentication) { String token = request.getHeader("token" ); if ( token != null ) { tokenManager.removeToken(token); String username = tokenManager.getUserInfoFromToken(token); redisTemplate.delete(username); } ResponseUtil.out(response, R.ok()); } }
自定义无权限访问时的处理器 UnauthEntryPoint
,当访问无权限时将执行 commence
方法:
1 2 3 4 5 6 public class UnauthEntryPoint implements AuthenticationEntryPoint { @Override public void commence (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { ResponseUtil.out(httpServletResponse, R.error()); } }
网关服务
订单服务