Spring Security使用记录
文章目录
- **前置知识**
- **1.概念介绍**
- **1.1权限管理**
- **1.2完成权限管理需要三个对象**
- **1.3Spring Security**
- **1.3.1创建web工程并导入jar包**
- **1.4Spring Security过滤器链**
- **1.4.1Spring Security常用过滤器链介绍**
- **1.4.2过滤器如何进行加载的?**
- **1.5UserDetailsService接口讲解**
- **1.6UserDetails**
- **1.7Authentication**
- **1.8AuthenticationManager**
- **1.9SecurityContext**
- **1.10SecurityContextHolder**
- **动态鉴权流程解析**
- **1.FilterSecurityInterceptor**
- **2.AccessDecisionManager**
- **动态鉴权的实现**
- **1.重新构造AccessDecisionManager**
- **2.自定义鉴权实现**
- **1.10WebSecurityConfigurerAdapter与@EnableWebSecurity**
- **1.11WebSecurityConfiguration**
- **1.9PasswordEncoder接口讲解**
- **1.10前后端分离应用中自定义token整合Spring Security**
- **2.Spring Security简单入门**
- **2.1XML配置**
- **2.1.1web.xml配置**
- **2.1.2spring-security.xml配置**
- **2.1.3Spring Security自定义页面配置**
- **2.2Spring Security的csrf防护机制**
- **2.3Spring Security流程认证分析**
- **2.3.1加密认证方式分析**
- **2.3.1.1加盐**
- **2.4remember功能基本实现**
- **2.5权限控制**
- **2.5.2异常处理方式**
- **2.6 Spring Security与Spring Boot整合使用**
- **2.6.2分布式访问**
- **2.6.3JWT介绍**
- **2.6.4非对称加密RSA介绍**
- **2.6.5SpringSecurity+JWT+RSA分布式认证思路分析**
- **2.6.6OAuth2介绍**
- **2.6.6.1授权码模式**
- **2.6.6.2简化模式(implicit)**
- **2.6.6.3密码模式(resource owner password credentials)**
- **2.6.6.4客户端模式(client credentials)**
- **2.7 Spring Security注解模式**
- **2.7.1@EnableWebSecurity和@EnableGlobalMethodSecurity**
- **3.SpringSecurity-web权限方案**
- **3.1设置登录系统的账号、密码**
- **4.SpringSecurity微服务权限方案**
- **4.2数据模型介绍**
- **4.3使用技术说明**
- **5.SpringSecurity源码剖析**
- **5.1认证流程**
- **5.2权限访问流程**
- **5.5SpringSecuirty请求间共享认证信息**
前置知识
1、掌握Spring框架
2、掌握SpringBoot使用
3、掌握JavaWeb技术
1.概念介绍
1.1权限管理
权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。权限管理几乎出现在任何系统中,前提是需要有用户和密码认证的系统。一般来说,Web应用的安全性包括**用户认证(Authentication)和用户授权(Authorization)**这两点,这两点也是Spring Security重要核心功能。
在权限管理的概念中,有两个重要的名词:
认证:通过用户名和密码成功登陆系统之后,让系统得到当前用户的角色身份。通俗点说就是系统认为用户是否能登录。
授权:系统根据当前用户的角色,给其授予对应可以操作的权限资源。验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以可以修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情
1.2完成权限管理需要三个对象
用户:主要包含用户名,密码和当前用户的角色信息,可以实现认证操作。
角色:主要包含角色名称,角色描述和当前角色拥有的权限信息,可以实现授权操作。
权限:权限也称为菜单,主要包含当前权限名称,url地址等信息,可以实现动态展示菜单。
注:这三个对象中,用户与角色是多对多的关系,角色与权限是多对多的关系,用户与权限没有直接关系,二者是通过角色来建立关联关系的。
1.3Spring Security
Spring Security是Spring采用AOP思想,基于servlet过滤器实现的安全框架。它提供了完善的认证机制和方法级的授权功能。
Spring Security本质上是一个过滤器链,即通过一层层的Filters来对web请求做处理
可以这样表述:
一个web请求会经过一条过滤器链,在经过过滤器链的过程中会完成认证和授权,如果中间发现这条请求未认证或者未授权,惠普官网根据被保护API的权限去抛出异常,然后由异常处理器去处理这些异常。
可以看下面这张图片
如上图,一个请求想要访问到API就会以从左到右的形式经过蓝线框框里面的过滤器,其中绿色部分是我们本篇主要将的负责认证的过滤器,蓝色部分负责异常处理,橙色部分则是负责授权。
图中的这两个绿色过滤器是Spring Security
就应该知道配置中有两个叫formLogin
、httpBasic
的配置项,在配置中打开了它俩就对应着打开了上面的过滤器。
formLogin
对应着form
表单认证方式,即UsernamePasswordAuthenticationFilter
httpBasic
对应着Basic认证方式,即BasicAuthenticationFilter
换言之,你配置了这两种认证方式,过滤器链才会加入它们,否则是不会被加到过滤器链中去的。
因为Spring Security
自带的过滤器是没有针对JWT这种认证方式的,所以我们的demo
中会写一个JWT的认证过滤器,然后放在绿色的位置进行认证工作。
SpringSecurity的重要概念
知道了Spring Security
的大致工作流程之后,还需要知道非常重要的组件。
- SecurityContext:上下文对象,
Authentication
对象会放在里面。 - SecurityContextHolder:用于拿到上下文对象的静态工具类。
- Authentication:认证接口,定义了认证对象的数据形式
- AuthenticationManager:用于校验
Authentication
,返回一个认证完成后的Authentication
对象。
SpringSecurity流程图
SpringSecurity流程图
流程说明
1、客户端发起一个请求,进入Spring Security
过滤器链
2、当到LogoutFilter
的时候判断是否是登出路径,如果是登出路径则到logoutHandler
,如果登出成功则到logoutSuccessHandler
登出成功处理,如果登出失败则由ExceptionTranslationFilter
;如果不是登出路径的话,则直接进入下一个过滤器。
3、当到UsernamePasswordAuthenticationFilter
的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作;如果登录失败,则到AuthenticationFailureHandler
登录失败处理器处理,如果登录成功则到AuthenticationSuccessHandler
登录成功处理器处理,如果不是登录请求则不进入该过滤器。
4、当到FilterSecurityInterceptor
的时候会拿到URI
,根据URI
去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到Controller
层,否则到AccessDeniedHandler
鉴权失败处理器处理。
1.3.1创建web工程并导入jar包
Spring Security主要jar包功能介绍:
spring-security-core.jar
核心包,任何Spring Security功能都需要此包
spring-secuirty-web.jar
web工程必备,包含过滤器和相关的Web安全基础结构代码。
spring-security-config.jar
用于解析xml配置文件,用到Spring Security的xml配置文件的就需要用到此包。
spring-security-taglibs.jar
Spring Security提供的动态标签库,jsp页面可以用。
1.4Spring Security过滤器链
Spring Security本质上是一个过滤器链
1.4.1Spring Security常用过滤器链介绍
Spring Security本质上是一个过滤器链
过滤器是典型的AOP思想,了解web工程的都知道。
1、org.springframework.security.web.context.SecurityContextPersistenceFilter
这是首当其冲的一个过滤器
SecurityContextPersistenceFilter主要是使用SecurityContextPersistenceFilter在session中保存或者更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。
SecurityContext存储了当前用户的认证以及权限信息。
2、org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager
3、org.springframework.security.web.header.HeaderWriterFilter
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
4、org.springframework.security.web.csrf.CsrfFilter
csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,如果不包含,则报错,起到防止csrf攻击的效果。
5、org.springframework.security.web.authentication.logout.LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息
6、org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求
对/login的POST请求做拦截,校验表单中用户名、密码
7、org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面
8、org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
由此过滤器可以生产一个默认的退出登录页面
9、org.springframework.security.web.authentication.www.BasicAuthenticationFilter
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
10、org.springframework.security.web.savedrequest.RequestCacheAwareFilter
通过HttpSessionRequestCache内部维护一个RequestCache,用于缓存HttpServletRequest
11、org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
针对ServletRequest进行了一次包装,使得request具有更加丰富的API
12、org.springframework.security.web.authentication.AnonymousAuthenticationFilter
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
13、org.springframework.security.web.session.SessionManagementFilter
SecurityContextRepository限制同一用户开启多个会话的数量
14、org.springframework.security.web.access.ExceptionTranslationFilter
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
是一个异常过滤器,用来处理在认证授权过程中抛出的异常
15、org.springframework.security.web.access.intercept.FilterSecurityInterceptor
是一个方法级的权限过滤器,基本位于过滤链的最底部
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。
1.4.2过滤器如何进行加载的?
Spring Boot已经帮我们配置了这些,因此不需要配置。但是这是基本的原理:
1.5UserDetailsService接口讲解
当什么也没有配置的时候,账号和密码是由Spring Security定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。
UserDetailsService
接口
package org.springframework.security.core.userdetails;/*** Core interface which loads user-specific data.* 加载用户特定数据的核心接口。* It is used throughout the framework as a user DAO and is the strategy used by the DaoAuthenticationProvider* 它作为用户DAO在整个框架中使用,也是DaoAuthenticationProvider使用的策略* The interface requires only one read-only method, which simplifies support for new* data-access strategies.* 该接口只需要一个只读方法,这简化了对新数据访问策略的支持。*/
public interface UserDetailsService {/*** 根据用户名定位用户。在实际实现中,搜索可能是区分大小写的,或者不区分大小写,这取决于实现实例的配置方式。在这种情况下,返回的UserDetails对象可能拥有与实际请求不同的用户名。* @param username 标识需要其数据的用户的用户名。* @return a fully populated user record (never <code>null</code>)* 一个完全填充的用户记录(从不null)* @throws UsernameNotFoundException if the user could not be found or the user has no GrantedAuthority* 如果找不到用户或用户没有被授予权限,则使用UsernameNotFoundException*/UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
从代码中可以看到,UserDetailsService
只提供了一个方法。从方法名loaduserByUsername
可以看出,该方法通过用户名来加载用户。我们在实现loadUserByUsername
方法的时候,就可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails
,(通常是一个org.springframework.security.core.userdetails.User
,它继承自UserDetails
) 并返回。
1.6UserDetails
package org.springframework.security.core.userdetails;import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;import java.io.Serializable;
import java.util.Collection;/*** Provides core user information.* 提供核心用户信息* Implementations are not used directly by Spring Security for security purposes. They* simply store user information which is later encapsulated into {@link Authentication}* objects. This allows non-security related user information (such as email addresses,* telephone numbers etc) to be stored in a convenient location.* 出于安全目的,Spring Security不会直接使用实现* 它们只是存储用户信息,这些信息稍后被封装到Authentication对象中。* 这允许非安全相关的用户信息(如email、电话号码等)被存储在一个方便的位置* Concrete implementations must take particular care to ensure the non-null contract* detailed for each method is enforced. See User for a reference implementation (which you might like to extend or use in your code).* 具体的实现必须特别小心,确保为每个方法详细说明的非空契约得到实施。* 有关参考实现(您可能想在代码中扩展或者使用它),请参阅User。*/
public interface UserDetails extends Serializable {//获取用户权限,本质上是用户的角色信息Collection<? extends GrantedAuthority> getAuthorities();//获取用户密码String getPassword();//获取用户名String getUsername();//账户是否过期boolean isAccountNonExpired();//账户是否被锁定boolean isAccountNonLocked();//密码是否过期boolean isCredentialsNonExpired();//账户可否可用boolean isEnabled();
}
1.7Authentication
Authentication
即验证,表明当前用户是谁。什么是验证,比如一组用户名和密码就是验证,当然错误的用户名和密码也是验证,只不过Spring Security会校验失败。
package org.springframework.security.core;import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;/*** Represents the token for an authentication request or for an authenticated principal* once the request has been processed by the* {@link AuthenticationManager#authenticate(Authentication)} method.* 表示身份验证请求或者经过身份验证的主体在请求已由身份验证方法处理后的令牌。* Once the request has been authenticated, the <tt>Authentication</tt> will usually be* stored in a thread-local <tt>SecurityContext</tt> managed by the* {@link SecurityContextHolder} by the authentication mechanism which is being used. An* explicit authentication can be achieved, without using one of Spring Security's* authentication mechanisms, by creating an <tt>Authentication</tt> instance and using* the code:*一旦请求被身份验证authticated,身份验证通常将被存储在一个thread-local的SecurityContext中,该thread-local由正在使用的身份验证机制来管理。不使用Spring Security的身份验证机制,通过创建一个身份验证实例并使用代码,就可以实现显式地身份验证。* SecurityContextHolder.getContext().setAuthentication(anAuthentication);** Note that unless the <tt>Authentication</tt> has the <tt>authenticated</tt> property* set to <tt>true</tt>, it will still be authenticated by any security interceptor (for* method or web invocations) which encounters it.* In most cases, the framework transparently takes care of managing the security context* and authentication objects for you.*/
public interface Authentication extends Principal, Serializable {Collection<? extends GrantedAuthority> getAuthorities();Object getCredentials();Object getDetails();Object getPrincipal();boolean isAuthenticated();void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Authentication
是一个接口,实现类都会定义authorities
、credentials
、details
、principal
、authenticated
等字段。具体含义如下:
- getAuthorities:获取用户权限,一般情况下获取到的是用户的角色信息
- getCredentials:获取证明用户认证的信息,通常情况下获取到的是密码等信息。
- getDetails:获取用户的额外信息,比如IP地址、经纬度等
- getPrincipal:获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是
UserDetails
(暂时理解为,当前应用用户对象的扩展)。 - isAuthenticated:获取当前
Authentication
是否已经认证。 - setAuthenticated:设置当前
Authentication
是否已经认证。
验证前:
principal
填充的是用户名,credentials
填充的是密码,details
填充的是用户的IP或者经纬度之类的信息。
验证后:- Spring Security对
Authentication
重新注入,principal
填充用户信息(包含用户名、年龄等),authorities
会填充用户的角色信息,Authenticated
会被设置为true。重新注入的Authentication
会被填充到SecurityContext
中。
1.8AuthenticationManager
AuthenticationManager
负责校验Authentication
对象。在AuthenticationManager
的authenticate
函数中,实现对Authentication
的校验。
如果校验通过,则返回一个重新注入的Authentication
对象;校验失败,则抛出AuthenticationException
异常。
Authentication authenticate(Authentication authentication)throws AuthenticationException;
}
AuthenticationManager
可以将异常抛出的更加明确:
- 当用户不可用时抛出 DisabledException
- 当用户被锁定时抛出 LockedException
- 当用户密码错误时抛出 BadCredentialsException
重新注入的Authentication
会包含当前用户的详细信息,并且被填充到SecurityContext
中,这样SpringSecurity
的验证流程就完成了,SpringSecurity
可以识别到"你是谁"。
1.9SecurityContext
1.10SecurityContextHolder
可以想象这四个部分如何串联成为一个完整的认证流程:
(1)、先是一个请求带着身份信息进来
(2)、经过AuthenticationManager
的认证,
(3)、再通过SecurityContextHolder
获取SecurityContext
(4)、最后将认证后的信息放入到SecurityContext
流程实例
若干组件:
- 定义加密器Bean
@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
这个Bean是必不可少的,Spring Security
在认证操作时会使用我们定义的这个加密器,如果没有则会出现异常。
- 定义AuthenticationManager
@Beanpublic AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();}
这里将Spring Security
自带的authenticationManager
声明成Bean,声明它的作用是用它帮我们进行认证操作,调用这个Bean的authenticate
方法会由Spring Security
自动帮助我们做认证。
- 实现UserDetailsService
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserService userService;@Autowiredprivate RoleInfoService roleInfoService;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {log.debug("开始登陆验证,用户名为: {}",s);// 根据用户名验证用户QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();queryWrapper.lambda().eq(UserInfo::getLoginAccount,s);UserInfo userInfo = userService.getOne(queryWrapper);if (userInfo == null) {throw new UsernameNotFoundException("用户名不存在,登陆失败。");}// 构建UserDetail对象UserDetail userDetail = new UserDetail();userDetail.setUserInfo(userInfo);List<RoleInfo> roleInfoList = roleInfoService.listRoleByUserId(userInfo.getUserId());userDetail.setRoleInfoList(roleInfoList);return userDetail;}
}
实现UserDetailsService
的抽象方法并返回一个UserDetails
对象,认证过程中SpringSecurity
会调用这个方法访问数据库进行对用户的搜索,逻辑什么的都可以自定义,无论是从数据库中还是从缓存中,但是我们需要将我们查询出来的用户信息和权限信息组装成为一个UserDetails
返回。
UserDetails
也是一个定义了数据形式的接口,用于保存我们从数据库中查出来的数据,其主要的功能时验证账号状态和获取权限。
- TokenUtil
由于我们是JWT的认证模式,所以我们也需要一个帮助我们操作Token的工具类,需要具有下面三个方法:
- 创建token
- 验证token
- 泛解析token中的信息
代码的具体实现
1、认证方法
访问一个系统,最先访问的都是认证方法,这里简写了简略的认证需要的步骤,因为在实际过程中还需要写登录记录、前台密码解密这些操作。
大概可以分为五个步骤:
1、传入用户名和密码创建UsernamePasswordAuthenticationToken
对象,就是我们前面说的Authentication
的实现类,传入用户名和密码做构造参数,这个对象就是我们创建出来的未认证的Authentication
对象。
2、使用我们先前已经声明过的Bean-authenticationManager
调用它的authenticate
方法进行认证,返回一个认证完成的Authentication
对象。
3、认证完成没有出现异常,就会走到第三步,使用SecurityContextHolder
获取SecurityContext
之后,将认证完成后的Authentication
对象,放入上下文对象。
4、从Authentication
对象中拿到我们的UserDetails
对象,之前我们说过的,认证后的Authentication
对象调用它的getPrincipal()
方法就可以拿到之前在数据库查询后组装出来的UserDetails
对象。然后创建token
。
5、把UserDetails
对象放入缓存中,方便后面过滤器使用。
这样的话就算完成了,其实主要的认证操作都会由authenticationManager.authenticate()
帮我们完成。
JWT过滤器
有了token之后,我们需要把过滤器放在过滤器链中,用于解析token,因为没有session,所以当每次去辨别这是哪个用户的请求的时候,都是根据请求中的token来解析出来当前是哪个用户。
所以当我们需要一个过滤器去拦截所有请求,前文我们也说过,这个过滤器会放在绿色部分用来替代UsernamePasswordAuthenticationFilter
,所以我们新建一个JwtAuthenticationTokenFilter
,然后将它注册为Bean
,并在编写配置文件的时候需要加上这个:
@Beanpublic JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {return new JwtAuthenticationTokenFilter();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.addFilterBefore(jwtAuthenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class);}
addFilterBefore
的语义是添加一个Filter
到xxxFilter
之前,放在这里就是把JwtAuthenticationTokenFilter
放在UsernamePasswordAuthenticationFilter
之前,因为filter
的执行也是有顺序的,我们必须要把我们的filter
放在过滤器链中绿色的部分才会起到自动认证的效果。
下面可以看看JwtAuthenticationTokenFilter
的具体实现了:
@Overrideprotected void doFilterInternal(@NotNull HttpServletRequest request,@NotNull HttpServletResponse response,@NotNull FilterChain chain) throws ServletException, IOException {log.info("JWT过滤器通过校验请求头token进行自动登录...");// 拿到Authorization请求头内的信息String authToken = jwtProvider.getToken(request);// 判断一下内容是否为空且是否为(Bearer )开头if (StrUtil.isNotEmpty(authToken) && authToken.startsWith(jwtProperties.getTokenPrefix())) {// 去掉token前缀(Bearer ),拿到真实tokenauthToken = authToken.substring(jwtProperties.getTokenPrefix().length());// 拿到token里面的登录账号String loginAccount = jwtProvider.getSubjectFromToken(authToken);if (StrUtil.isNotEmpty(loginAccount) && SecurityContextHolder.getContext().getAuthentication() == null) {// 缓存里查询用户,不存在需要重新登陆。UserDetail userDetails = caffeineCache.get(CacheName.USER, loginAccount, UserDetail.class);// 拿到用户信息后验证用户信息与tokenif (userDetails != null && jwtProvider.validateToken(authToken, userDetails)) {// 组装authentication对象,构造参数是Principal Credentials 与 Authorities// 后面的拦截器里面会用到 grantedAuthorities 方法UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());// 将authentication信息放入到上下文对象中SecurityContextHolder.getContext().setAuthentication(authentication);log.info("JWT过滤器通过校验请求头token自动登录成功, user : {}", userDetails.getUsername());}}}chain.doFilter(request, response);}
下面简单看看逻辑步骤:
1、拿到Authorization
请求头对应的token信息。
2、去掉token的头部(Bearer)
3、解析token,拿到存放的登录账号
4、因为已经完成过登录,所以可以直接从缓存中拿到我们的UserDetail
信息即可。
5、查看UserDetail是否为null,以及查看token是否过期,UserDetail
用户名与token
中的是否一致。
6、组装一个authentication
对象,把它放在上下文对象中,这样后面的过滤器看到我们的上下文对象中有authentication
对象,就相当于以及认证过了。
这样的话,每当一个带有正确token的请求进来之后,都会找到它的账号信息,并放在上下文对象中,可以看到使用SecurityContextHolder
很方便的拿到上下文对象中的Authentication
对象。
完成之后,启动Demo,可以看到过滤器链中有以下的过滤器,其中我们自定义的是第五个:
所以最后,我们登录完成之后,把获取到的账号信息与角色信息放入缓存中之后,当带着token的请求到来时,我们就把它从缓存中取出,再次放到缓存对象中去。
因此,逻辑链条就变成了:
登录-->
拿到token-->
请求带上token-->
JWT过滤器-->
校验token-->
将从缓存中查出来的对象放到上下文中
这样,认证逻辑就算完成了。
代码优化
认证和JWT过滤器完成后,这个JWT的项目其实已经可以运行了。但是为了代码的鲁棒性,可以增加辅助的功能代码。
1、认证失败处理器
用户未登录或者token解析失败的时候会触发这个处理器,返回一个非法访问的结果。
2、权限不足处理器
当用户本身权限不满足所访问API需要的权限的时候,触发这个处理器,返回一个权限不足的结果。
3、退出方法
用户退出一般就是清掉上下文对象和缓存就行了,也可以做附加操作,这两步是必须的。
4、token刷新
JWT的项目token刷新也是必不可少的,这里刷新token的方法放在token工具箱中,刷新完成之后把缓存重载一遍就可以了。缓存是有有效期的,重新put可以重置失效时间。
动态鉴权流程解析
认证过程主要围绕图中过滤连的绿色部分,动态鉴权则是围绕其中的橙色部分,也就是FilterSecurityInterceptor
。
1.FilterSecurityInterceptor
一个请求完成认证之后,且没有抛出异常的情况下就会到达FilterSecurityInterceptor
所负责的鉴权部分,也就是说鉴权的入口就在FilterSecurityInterceptor
。
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {FilterInvocation fi = new FilterInvocation(request, response, chain);this.invoke(fi);}
}
上文代码可以看出FilterSecurityInterceptor
是实现了AbstractSecurityInterceptor
,其中AbstractSecurityInterceptor
预先写好了一段很重要的代码。
FilterSecurityInterceptor
的主要方法是doFilter
方法,过滤器的特性就是请求过来之后都会执行doFilter
方法,FilterSecurityInterceptor
的doFilter
方法很简单,只有两行。
第一行创建了一个FilterInvocation
对象,这个FilterInvocation
对象你可以当作它封装了request
,它的主要工作就是拿请求中的信息,比如请求的URI。
第二行调用了自身的invoke
方法,并将FilterInvocation
对象传入。
所以显然,主要的逻辑就在invoke
方法中,可以看看:
public void invoke(FilterInvocation fi) throws IOException, ServletException {if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());} else {if (fi.getRequest() != null && this.observeOncePerRequest) {fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);}InterceptorStatusToken token = super.beforeInvocation(fi);try {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());} finally {super.finallyInvocation(token);}super.afterInvocation(token, (Object)null);}}
invoke
方法的主逻辑是一个if-else
,一般来说不满足if中三个条件的,都会知性逻辑来到else分支。
else分支又可以继续分为两个部分:
1、调用了super.beforeInvocation(fi)
。
2、调用完成之后过滤器继续往下走
第二步可以不看,每个过滤器都有这么一步,主要查看super.beforeInvocation(fi)
。前面已经提到,FilterSecurityInterceptor
实现了抽象类AbstractSecurityInterceptor
,所以这里super指代的是AbstractSecurityInterceptor
,那super.beforeInvocation(fi)
其实指的就是AbstractSecurityInterceptor.beforeInvocation(fi)
。
protected InterceptorStatusToken beforeInvocation(Object object) {Assert.notNull(object, "Object was null");final boolean debug = logger.isDebugEnabled();if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {throw new IllegalArgumentException("Security invocation attempted for object "+ object.getClass().getName()+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "+ getSecureObjectClass());}Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);Authentication authenticated = authenticateIfRequired();// Attempt authorizationtry {this.accessDecisionManager.decide(authenticated, object, attributes);}catch (AccessDeniedException accessDeniedException) {publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException));throw accessDeniedException;}// Attempt to run as a different userAuthentication runAs = this.runAsManager.buildRunAs(authenticated, object,attributes);if (runAs == null) {if (debug) {logger.debug("RunAsManager did not change Authentication object");}// no further work post-invocationreturn new InterceptorStatusToken(SecurityContextHolder.getContext(), false,attributes, object);}else {if (debug) {logger.debug("Switching to RunAs Authentication: " + runAs);}SecurityContext origCtx = SecurityContextHolder.getContext();SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());SecurityContextHolder.getContext().setAuthentication(runAs);// need to revert to token.Authenticated post-invocationreturn new InterceptorStatusToken(origCtx, true, attributes, object);}}
源码较长,这里精简了其中的一部分,这段代码可以大致分为三步:
1、拿到了Collection<ConfigAttribute>
对象,这个对象是一个List
,其实里面就是我们在配置文件中配置的过滤规则。
2、拿到了Authentication
,这里是调用authenticateIfRequired
方法拿到了,其实里面还是通过SecurityContextHolder
拿到的。
private Authentication authenticateIfRequired() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication.isAuthenticated() && !alwaysReauthenticate) {if (logger.isDebugEnabled()) {logger.debug("Previously Authenticated: " + authentication);}return authentication;}authentication = authenticationManager.authenticate(authentication);// We don't authenticated.setAuthentication(true), because each provider should do// thatif (logger.isDebugEnabled()) {logger.debug("Successfully Authenticated: " + authentication);}SecurityContextHolder.getContext().setAuthentication(authentication);return authentication;}
3、调用了try {this.accessDecisionManager.decide(authenticated, object, attributes);}
,前面两步都是对decide
方法做参数的准备,第三步才是进入鉴权的逻辑,既然这里面才是真正鉴权的逻辑,也就是说其实鉴权是accessDecisionManager
在做。
2.AccessDecisionManager
在前面的源码中我们看到了鉴权真正的处理者,AccessDecisionManager
,这里面一层一层就像套娃。
AccessDecisionManager
是一个接口,定义如下:
public interface AccessDecisionManager {// 主要鉴权方法void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,InsufficientAuthenticationException;boolean supports(ConfigAttribute attribute);boolean supports(Class<?> clazz);
}
AccessDecisionManager
是一个接口,声明了三个方法,除了第一个鉴权方法之外,还有两个是辅助性的方法,其作用都是鉴别decide
方法中参数的有效性。
既然是接口,上文中所调用的就是它的实现类了,看看接口的结构树:
上图中有三个实现类,分别代表了三种不同的鉴权逻辑:
- AffirmativeBased:一票通过,只要有一票通过就算通过,默认是它。
- UnanimousBased:一票反对,只要有一票反对就不能通过。
- ConcensusBased:少数票服从多数票。
这里的表述为什么要用票呢?因为在实现类里面采用了委托的形式,将请求委托给投票器,每个投票器拿着这个请求根据自身的逻辑来计算能否通过然后进行投票,所以会有上面的表述。
也就是说这三个实现类,其实还不是真正判断请求能不能通过的实现类,真正判断请求是否通过的是投票器,然后实现类把投票器的结果综合起来决定到底能否通过。
实现类将投票器的结果综合起来进行决定,即投票器可以放入多个实现类。每个实现类中的投票器数量取决于构造的时候放入了多少投票器。
public class AffirmativeBased extends AbstractAccessDecisionManager {public AffirmativeBased(List<AccessDecisionVoter<? extends Object>> decisionVoters) {super(decisionVoters);}//拿到所有的投票器,循环遍历进行投票public void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {int deny = 0;for (AccessDecisionVoter voter : getDecisionVoters()) {int result = voter.vote(authentication, object, configAttributes);if (logger.isDebugEnabled()) {logger.debug("Voter: " + voter + ", returned: " + result);}switch (result) {case AccessDecisionVoter.ACCESS_GRANTED:return;case AccessDecisionVoter.ACCESS_DENIED:deny++;break;default:break;}}if (deny > 0) {throw new AccessDeniedException(messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));}// To get this far, every AccessDecisionVoter abstainedcheckAllowIfAllAbstainDecisions();}
}
AffirmativeBased
的构造是传入投票器List
,其主要的鉴权逻辑交给投票器去判断,投票器返回不同的数字代表不同的结果,然后AffirmativeBased
根据自身一票通过的策略决定是放行还是抛出异常。
AffirmativeBased
默认传入的构造器只有一个->WebExpressionVoter
,这个构造器会根据你在配置文件中的配置进行逻辑处理得出投票结果。
所以SpringSecurity
默认的鉴权逻辑就是根据配置文件中的配置进行鉴权,这是符合我们现有认知的。
动态鉴权的实现
通过上面的内容,应该可以理解SpringSecurity到底是如何进行鉴权的,那么如何做到动态的给予某个角色不同的访问权限应该怎么做呢?
既然是动态鉴权,那我们的权限URI肯定存放在数据库中,我们要做的就是实时的在数据库中读取不同角色对应的权限然后与当前登录的用户做个比较。
这里就可以想到一些方案,比如:
- 直接重写一个
AccessDecisionManager
,将它用作默认的AccessDecisionManager
,并在里面直接写好鉴权逻辑。 - 再比如重写一个投票器,将它放到默认的
AccessDecisionManager
中,和之前一样用投票器进行鉴权。 - 还可以直接对
FilterSecurityInterceptor
进行改动
那么我们需要写一个新的投票器,在这个投票器中拿到当前用户用户的角色,并将其和当前请求所需要的角色进行对比。
单是这样还不够,因为我们在配置文件中也配置了一些放行的权限,比如登录URI就是直接放行的,所以还需要继续使用我们上文中提到的WebExpressionVoter
,也就是说要自定义权限+配置文件双行的模式,所以我们的AccessDecisionManager
中就存在两个投票器:WebExpressionVoter
和自定义的投票器。
紧接着还需要去考虑使用什么样的投票策略,这里使用的是UnanimousBased
一票反对策略,而没有使用默认的一票通过策略,因为我们的配置中配置了除了登录请求以外的其它请求都需要认证的,这个逻辑会被WebExpressionVoter
处理,如果使用了一票通过策略,那么我们去访问被保护的API的时候,WebExpressionVoter
发现当前请求认证了,就直接投赞成票,且因为是一票通过策略,这个请求就走不到我们自定义的投票器了。
也可以不使用配置文件中的配置,将你的自定义权限配置都放在数据库中,然后统一交给一个投票器处理。
1.重新构造AccessDecisionManager
首先重新构造AccessDecisionManager
,因为投票器是系统启动的时候自动添加进去的,所以我们想多加入一个构造器必须自己重新构建AccessDecisionManager
,然后将它放到配置中去。
而且我们的投票策略已经改变了,要由AffirmativeBased
换成UnanimousBased
,所以这一步是不可缺少的。
并且我们还要自定义一个投票器,将它注册成为Bean
。AccessDecisionProcessor
就是我们需要自定义的投票器。
@Beanpublic AccessDecisionVoter<FilterInvocation> accessDecisionProcessor() {return new AccessDecisionProcessor();}@Beanpublic AccessDecisionManager accessDecisionManager() {// 构造一个新的AccessDecisionManager 放入两个投票器List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(new WebExpressionVoter(), accessDecisionProcessor());return new UnanimousBased(decisionVoters);}
定义完AccessDecisionManager
之后,我们将它放入启动配置:
@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()// 放行所有OPTIONS请求.antMatchers(HttpMethod.OPTIONS).permitAll()// 放行登录方法.antMatchers("/api/auth/login").permitAll()// 其他请求都需要认证后才能访问.anyRequest().authenticated()// 使用自定义的 accessDecisionManager.accessDecisionManager(accessDecisionManager()).and()// 添加未登录与权限不足异常处理器.exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler()).authenticationEntryPoint(restAuthenticationEntryPoint()).and()// 将自定义的JWT过滤器放到过滤链中.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)// 打开Spring Security的跨域.cors().and()// 关闭CSRF.csrf().disable()// 关闭Session机制.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);}
这样之后,SpringSecurity
里面的AccessDecisionManager
就会被替换成我们自定义的AccessDecisionManager
了。
2.自定义鉴权实现
上文配置中放入了两个投票器,其中第二个投票器就是我们需要创建的投票器,命名为AccessDecisionProcessor
。
投票其实也是有一个接口规范的,我们只需要实现这个AccessDecisionVoter
接口就行了,然后实现它的方法。
@Slf4j
public class AccessDecisionProcessor implements AccessDecisionVoter<FilterInvocation> {@Autowiredprivate Cache caffeineCache;@Overridepublic int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> attributes) {assert authentication != null;assert object != null;// 拿到当前请求uriString requestUrl = object.getRequestUrl();String method = object.getRequest().getMethod();log.debug("进入自定义鉴权投票器,URI : {} {}", method, requestUrl);String key = requestUrl + ":" + method;// 如果没有缓存中没有此权限也就是未保护此API,弃权PermissionInfoBO permission = caffeineCache.get(CacheName.PERMISSION, key, PermissionInfoBO.class);if (permission == null) {return ACCESS_ABSTAIN;}// 拿到当前用户所具有的权限List<String> roles = ((UserDetail) authentication.getPrincipal()).getRoles();if (roles.contains(permission.getRoleCode())) {return ACCESS_GRANTED;}else{return ACCESS_DENIED;}}@Overridepublic boolean supports(ConfigAttribute attribute) {return true;}@Overridepublic boolean supports(Class<?> clazz) {return true;}
}
大致逻辑是这样:使用URI+METHOD
作为key,去缓存中直接查找权限相关的信息,如果没有找到此URI,则证明这个URI没有被保护,投票器可以直接弃权。
如果找到了这个URI相关权限信息,则使用其与用户自带的角色信息做一个对比,根据结果返回ACCESS_GRANTED
或者ACCESS_DENIED
。
当然,这样做的前提是,那就是在系统启动的时候就把URI权限数据都放到缓存中了,系统一般在启动的时候都会把热点数据放入缓存中,以提高系统的访问效率。
当然这样做有一个前提,那就是在系统启动的时候就把URI权限数据都放到缓存中了,系统一般在启动的时候都会把热点数据放入缓存中,以提高系统的访问效率。
@Component
public class InitProcessor {@Autowiredprivate PermissionService permissionService;@Autowiredprivate Cache caffeineCache;@PostConstructpublic void init() {List<PermissionInfoBO> permissionInfoList = permissionService.listPermissionInfoBO();permissionInfoList.forEach(permissionInfo -> {caffeineCache.put(CacheName.PERMISSION, permissionInfo.getPermissionUri() + ":" + permissionInfo.getPermissionMethod(), permissionInfo);});}
}
考虑到权限URI非常多,所以将权限URI作为key放到缓存中,因为一般缓存中通过读取key读取数据的速度是O(1),所以这样会非常的快速。
鉴权的逻辑到底是如何处理的,其实是开发者自己来定义的,要根据系统需求和数据库表设计进行综合的考量,这里只是给出一个思路。
如果你一时没有理解上面权限URI做key的思路的话,可以再举一个例子。
比如,你也可以拿到当前用户的角色,查到这个角色下的所有能访问的URI,然后比较当前请求的URI,有一致的则证明当前用户的角色下包含这个URI的权限所以可以放行,没有一致的则证明不够权限不能放行。
这种方式的话去比较URI的时候可能遇到这种问题:我当前角色权限是/api/user/**
,而我请求的URI是/user/get/1
,这种Ant
风格的权限定义方式,可以使用一个工具类来进行比较:
@Testpublic void match() {AntPathMatcher antPathMatcher = new AntPathMatcher();// trueSystem.out.println(antPathMatcher.match("/user/**", "/user/get/1"));}
这是我问了测试直接new了一个AntPathMatcher
,实际中可以将它注册成Bean
,注入到AccessDecisionProcessor
中进行使用。
也可以比较RESTFUL风格的URI,比如:
@Testpublic void match() {AntPathMatcher antPathMatcher = new AntPathMatcher();// trueSystem.out.println(antPathMatcher.match("/user/{id}", "/user/1"));}
在面对真正的系统的时候,往往是根据系统设计进行组合使用这些工具类和设计思想。
注:ACCESS_GRANTED
,ACCESS_DENIED
和ACCESS_ABSTAIN
是AccessDecisionVoter
接口中带有的常量。
SpringSecurity动态鉴权流程解析
Spring Security 入门原理及实战
Spring Security 的 Web 应用和指纹登录实践
1.10WebSecurityConfigurerAdapter与@EnableWebSecurity
一般而言,使用SpringSecurity的时候都会新建一个与SpringSecurity
的配置类,用它继承WebSecurityConfigurerAdapter
,然后打上注解@EnableWebSecurity
,就可以通过重写WebSecurityConfigurerAdapter
里面的方法来完成我们自己定义的配置。
就像这样:
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {}}
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,SpringWebMvcImportSelector.class,OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {/*** Controls debugging support for Spring Security. Default is false.* @return if true, enables debug support with Spring Security*/boolean debug() default false;
}
现在我们来具体看看@EnableWebSecurity
:
@Import
注解是SpringBoot中用于引入外部配置的注解,可以理解为:@EnableWebSecurity
注解激活了@Import
注解中包含的配置类。SpringWebMvcImportSelector
的作用是判断当前的环境是否包含springmvc
,因为Spring Security
可以在非Spring环境下使用,为了避免DispatcherServlet
的重复配置,所以使用了这个注解来区分。WebSecurityConfiguration
顾名思义,是用来配置web安全的。
@EnableGlobalAuthentication
注解的源码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({AuthenticationConfiguration.class})
@Configuration
public @interface EnableGlobalAuthentication {}
同样要注意其中的@Import
注解,它实际上激活了AuthenticationConfiguration
这样一个配置类,用来配置认证相关的核心类。
也就是说:@EnableWebSecurity
注解实际上的工作就是加载了WebSecurityConfiguration
和AuthenticationConfiguration
这两个核心配置类,也就此将Spring Security
的职责划分为了配置安全信息、配置认证信息两个部分。
1.11WebSecurityConfiguration
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {private WebSecurity webSecurity;private Boolean debugEnabled;private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;private ClassLoader beanClassLoader;@Autowired(required = false)private ObjectPostProcessor<Object> objectObjectPostProcessor;@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)public Filter springSecurityFilterChain() throws Exception {boolean hasConfigurers = webSecurityConfigurers != null&& !webSecurityConfigurers.isEmpty();if (!hasConfigurers) {WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {});webSecurity.apply(adapter);}return webSecurity.build();}@Autowired(required = false)public void setFilterChainProxySecurityConfigurer(ObjectPostProcessor<Object> objectPostProcessor,@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)throws Exception {webSecurity = objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor));if (debugEnabled != null) {webSecurity.debug(debugEnabled);}webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);Integer previousOrder = null;Object previousConfig = null;for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {Integer order = AnnotationAwareOrderComparator.lookupOrder(config);if (previousOrder != null && previousOrder.equals(order)) {throw new IllegalStateException("@Order on WebSecurityConfigurers must be unique. Order of "+ order + " was already used on " + previousConfig + ", so it cannot be used on "+ config + " too.");}previousOrder = order;previousConfig = config;}for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {webSecurity.apply(webSecurityConfigurer);}this.webSecurityConfigurers = webSecurityConfigurers;}}
首先WebSecurityConfiguration
是一个配置类,类上面打了@Configuration
注解,这个注解的实际作用就是把这个类中所有带@Bean
注解的Bean给实例化一下。
这里先看setFilterChainProxySecurityConfigurer
这个方法,因为它是@Autowired
注解,比springSecurityFilterChain
方法优先执行,从系统加载的顺序来看,我们需要先看它。
@Autowired
在这里的作用是为这个方法自动注入所需要的两个参数,我们先来看看这两个参数:
- 参数
objectPostProcessor
是为了创建WebSecurity
实例而注入的,先了解即可。 - 参数
webSecurityConfigurers
是一个List,它实际上是所有WebSecurityConfigurerAdapter
的子类,那如果我们定义了自定义的配置类,其实就是把我们的配置也读取到了。这里其实也难懂为什么参数中SecurityConfigurer<Filter, WebSecurity>
这个类型可以拿到WebSecurityConfigurerAdapter
的子类? - 因为
WebSecurityConfigurerAdapter
实现了WebSecurityConfigurer<WebSecurity>
接口,而WebSecurityConfigurer<WebSecurity>
又继承了SecurityConfigurer<Filter, T>
,经过一层实现和一层继承关系之后,WebSecurityConfigurerAdapter
终于成为了SecurityConfigurer
的子类。而参数中SecurityConfigurer<Filter, WebSecurity>
中的两个泛型参数其实是起到了一个过滤的作用,仔细查看我们的WebSecurityConfigurerAdapter
的实现与继承关系,可以发现我们的WebSecurityConfigurerAdapter
正好是这种类型。
@Autowired(required = false)public void setFilterChainProxySecurityConfigurer(ObjectPostProcessor<Object> objectPostProcessor,@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)throws Exception {//创建一个webSecurity实例webSecurity = objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor));if (debugEnabled != null) {webSecurity.debug(debugEnabled);}//根据order排序Collections.sort(webSecurityConfigurers, AnnotationAwareOrderComparator.INSTANCE);Integer previousOrder = null;Object previousConfig = null;for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {Integer order = AnnotationAwareOrderComparator.lookupOrder(config);if (previousOrder != null && previousOrder.equals(order)) {throw new IllegalStateException("@Order on WebSecurityConfigurers must be unique. Order of "+ order + " was already used on " + previousConfig + ", so it cannot be used on "+ config + " too.");}previousOrder = order;previousConfig = config;}//保存配置for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {webSecurity.apply(webSecurityConfigurer);}//成员变量初始化this.webSecurityConfigurers = webSecurityConfigurers;}
根据我们的注释,可以认为这段代码做的事情可以分为以下几步:
- 创建一个
webSecurity
实例,并且赋值给成员变量。 - 紧接着对
webSecurityConfigurers
通过order
进行排序,order
是加载顺序。 - 进行判断是否有相同
order
的配置类,如果出现将会直接报错。 - 保存配置,将其放入
webSecurity
的成员变量中。
可以将这些理解为成员变量的初始化、加载我们的配置类配置即可,因为后面的操作都是围绕初始化的webSecurity
实例和我们加载的配置类信息来做的。
SpringSecurityFilterChain
初始化完变量、加载完变量,就要开始构建过滤器链了。所以先走setFilterChainProxySecurityConfigurer
是有原因的,如果我们不把自定义配置加载进来,创建过滤器链的时候就不知道哪些过滤器需要或者不需要了。
/*** Creates the Spring Security Filter Chain* @return the {@link Filter} that represents the security filter chain* @throws Exception*/@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)public Filter springSecurityFilterChain() throws Exception {boolean hasConfigurers = webSecurityConfigurers != null&& !webSecurityConfigurers.isEmpty();if (!hasConfigurers) {WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {});webSecurity.apply(adapter);}return webSecurity.build();}
springSecurityFilterChain
方法逻辑就很简单了,如果我们没加载自定义的配置类,它就替我们加载一个默认的配置类,然后调用这个build
方法。
看到这个熟悉的方法名称,就知道这是建造者模式。点进去之后可以看到如下代码:
public final O build() throws Exception {if (this.building.compareAndSet(false, true)) {this.object = doBuild();return this.object;}throw new AlreadyBuiltException("This object has already been built");}
build()
方法是webSecurity
的父类AbstractSecurityBuilder
中的方法,这个方法又调用了doBuild()
方法。
@Overrideprotected final O doBuild() throws Exception {synchronized (configurers) {buildState = BuildState.INITIALIZING;//空方法beforeInit();//调用init方法init();buildState = BuildState.CONFIGURING;//空方法beforeConfigure();//调用configure方法configure();buildState = BuildState.BUILDING;//调用performBuildO result = performBuild();buildState = BuildState.BUILT;return result;}}
通过注释可以看到beforeInit()
和beforeConfigure()
都是空方法,实际上有用的只是init()
、configure()
、peformBuild()
方法。先来看看init()
、configure()
方法。
@SuppressWarnings("unchecked")private void init() throws Exception {Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();for (SecurityConfigurer<O, B> configurer : configurers) {configurer.init((B) this);}for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) {configurer.init((B) this);}}@SuppressWarnings("unchecked")private void configure() throws Exception {Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();for (SecurityConfigurer<O, B> configurer : configurers) {configurer.configure((B) this);}}
从源代码中可以看到的都是先获取到我们的配置类信息,然后循环调用配置类自己的init()
、configure()
方法。
前面说过,配置类都是继承了WebSecurityConfigurerAdapter
的子类,而WebSecurityConfigurerAdapter
又是SecurityConfigurer
的子类,所以SecurityConfigurer
的子类都需要实现inti()
、configure()
方法。
所以这里的init()
、configure()
方法其实就是调用WebSecurityConfigureAdapter
自己重写的init()
、configure()
方法。
又WebSecurityConfigureAdapter
中的configure()
方法是一个空方法,所以我们只需要看看WebSecurityConfigureAdapter
中的init()
方法就好。
public void init(final WebSecurity web) throws Exception {final HttpSecurity http = getHttp();web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() {public void run() {FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class);web.securityInterceptor(securityInterceptor);}});}
这里也分为两步:
(1)、执行了getHttp()
方法,这里面初始化加入了很多过滤器
(2)、将HttpSecurity
放入WebSecurity
,将FilterSecurityInterceptor
放入WebSecurity
,就是我们鉴权那章讲过的FilterSecurityInterceptor
。
这里主要看第一步getHttp()
方法:
@SuppressWarnings({ "rawtypes", "unchecked" })protected final HttpSecurity getHttp() throws Exception {if (http != null) {return http;}DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor.postProcess(new DefaultAuthenticationEventPublisher());localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);AuthenticationManager authenticationManager = authenticationManager();authenticationBuilder.parentAuthenticationManager(authenticationManager);authenticationBuilder.authenticationEventPublisher(eventPublisher);Map<Class<? extends Object>, Object> sharedObjects = createSharedObjects();http = new HttpSecurity(objectPostProcessor, authenticationBuilder,sharedObjects);if (!disableDefaults) {// @formatter:offhttp.csrf().and().addFilter(new WebAsyncManagerIntegrationFilter()).exceptionHandling().and().headers().and().sessionManagement().and().securityContext().and().requestCache().and().anonymous().and().servletApi().and().apply(new DefaultLoginPageConfigurer<>()).and().logout();// @formatter:onClassLoader classLoader = this.context.getClassLoader();List<AbstractHttpConfigurer> defaultHttpConfigurers =SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {http.apply(configurer);}}//我们一般重写这个方法configure(http);return http;}
getHttp()
方法里面http
调用的那一堆方法都是一个个的过滤器,第一个csrf()
明显就是防止CSRF攻击的过滤器。这就是SpringSecurity
默认会加入过滤器链的那些过滤器了。
其次,还有一个重点就是倒数第二行代码,这里也加上了注释,一般在自定义的配置类中重写的就是这个方法,所以我们自定义配置就是在这里生效的。
所以在初始化的过程中,这个方法会先加载自己默认的配置然后再加载我们重写的配置,这样两者结合起来,就变成了我们看到的默认配置。(如果我们不重写configure(http)
方法,也会加载默认配置。)
在init()
、configure()
(空方法)结束之后,就是调用performBuild()
方法。
@Overrideprotected Filter performBuild() throws Exception {Assert.state(!securityFilterChainBuilders.isEmpty(),() -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "+ "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "+ "More advanced users can invoke "+ WebSecurity.class.getSimpleName()+ ".addSecurityFilterChainBuilder directly");int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);for (RequestMatcher ignoredRequest : ignoredRequests) {securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));}//调用securityFilterChainBuilder的build()方法for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {securityFilterChains.add(securityFilterChainBuilder.build());}FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);if (httpFirewall != null) {filterChainProxy.setFirewall(httpFirewall);}filterChainProxy.afterPropertiesSet();Filter result = filterChainProxy;if (debugEnabled) {logger.warn("\n\n"+ "********************************************************************\n"+ "********** Security debugging is enabled. *************\n"+ "********** This may include sensitive information. *************\n"+ "********** Do not use in a production system! *************\n"+ "********************************************************************\n\n");result = new DebugFilter(filterChainProxy);}postBuildAction.run();return result;}
这个方法主要需要看的是调用securityFilterChainBuilder
的build()
方法,这个securityFilterChainBuilder
是我们在init()
方法中add的那个,所以这里的securityFilterChainBuilder
其实就是HttpSecurity
,所以这里其实是调用了HttpSecurity
的build()
方法。
HttpSecurity
的build()
方法进程和之前的一样,也是先init()
然后configure()
最后performBuild()
方法,值得一提的是,在HttpSecurity
的performBuild()
方法里面,会对过滤器链中的过滤器进行排序。
@Overrideprotected DefaultSecurityFilterChain performBuild() {filters.sort(comparator);return new DefaultSecurityFilterChain(requestMatcher, filters);}
HttpSecurity
的build()
方法执行完了之后将DefaultSecurityFilterChain
返回给WebSecurity
的performBuil()
方法,performBuil()
方法再将其转换为FilterChainProxy
,最后WebSecurity
的performBuild()
方法执行结束,返回一个Filter
注入成为name="springSecurityFilterChain"
的Bean
。
经过上述这些步骤之后,springSecurityFilterChain
方法执行完毕,我们的过滤器链就创建完成了,SpringSecurity
也可以跑起来了。
SpringSecurity启动流程源码解析
1.9PasswordEncoder接口讲解
1.10前后端分离应用中自定义token整合Spring Security
实际的前后端项目都是无状态的,并没有登录状态保持,服务器通过客户端调用传递的token来识别调用者是谁。
通常我们的系统流程是这样的:
- 客户端(react前端、IOS、安卓)调用"登录接口"获得一个包含token的响应(通常是JSON,如
{“token”:"abcd","expires":123456}
) - 客户端获取数据,并携带token参数
- 服务端根据token发现token过期/错误,返回"请登录"状态码
- 服务器发现token正常,并解析出是A,返回A的数据
- …
如果我们想在Spring Security
项目中使用自定义的token,那么我们需要思考下面的问题:
- 怎么发
token
(“即怎么登录?”) - 发token怎么和Spring Security整合。
- Spring Security怎么根据token得到授权认证信息。
下面从登录发token
开始,这里需要使用到UsernamePasswordAuthenticationToken
和SecurityContextHolder
,代码如下:
@RequestMapping(value = "/authenticate",method = RequestMethod.POST)public Token authorize(@RequestParam String username, @RequestParam String password) {// 1 创建UsernamePasswordAuthenticationTokenUsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);// 2 认证Authentication authentication = this.authenticationManager.authenticate(token);// 3 保存认证信息SecurityContextHolder.getContext().setAuthentication(authentication);// 4 加载UserDetailsUserDetails details = this.userDetailsService.loadUserByUsername(username);// 5 生成自定义tokenreturn tokenProvider.createToken(details);}@Injectprivate AuthenticationManager authenticationManager;
上面代码中1、2、3、4步骤都是和spring security
交互的,只有第5步是自己定义的,这里tokenProvider
就是我们系统中token的生成方式(这个完全是自己编写的,个性化的,通常是个加密串,通常可能会包含用户信息、过期时间等)。其中的Token
也是我们自定义的返回对象,其中包含的token
信息类似{"token":"abcd","expires":1234567890}
。
我们的tokenProvider
通常至少有两个方法,即:生成token、验证token。
public class TokenProvider {private final String secretKey;private final int tokenValidity;public TokenProvider(String secretKey, int tokenValidity) {this.secretKey = secretKey;this.tokenValidity = tokenValidity;}// 生成tokenpublic Token createToken(UserDetails userDetails) {long expires = System.currentTimeMillis() + 1000L * tokenValidity;String token = computeSignature(userDetails, expires);return new Token(token, expires);}// 验证tokenpublic boolean validateToken(String authToken, UserDetails userDetails) {check tokenreturn true or false;}// 从token中识别用户public String getUserNameFromToken(String authToken) {// ……return login;}public String computeSignature(UserDetails userDetails, long expires) {// 一些特有的信息组装 ,并结合某种加密活摘要算法return 例如 something+"|"+something2+MD5(s);}}
2.Spring Security简单入门
2.1XML配置
2.1.1web.xml配置
<filter><filter-name>encodingFilter</filter-name><filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class><init-param><param-name>encoding</param-name><param-value>UTF-8</param-value></init-param></filter><filter-mapping><filter-name>encodingFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping><!--存在两个容器,一个是spring容器,一个是springmvc容器--><servlet><servlet-name>springmvc</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring-mvc.xml</param-value></init-param></servlet><servlet-mapping><servlet-name>springmvc</servlet-name><url-pattern>/</url-pattern></servlet-mapping><!--spring容器--><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:applicationContext.xml</param-value></context-param><!--SpringSecurity核心过滤器链--><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><!--注意添加下面的参数--><absolute-ordering/>
2.1.2spring-security.xml配置
<!--配置springSecurity--><!--auto-config="true" 表示自动加载springsecurity的配置文件use-expression="true" 表示使用spring的el表达式来配置springsecurity--><security:http auto-config="true" use-expressions="true"><!--拦截资源--><!--pattern="/**" 表示拦截所有资源access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER角色才可以访问--><security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')" /></security:http><!--设置Spring Security认证用户信息的来源--><!--springsecurity默认的认证必须是加密的,加上{noop}表示不加密认证--><security:authentication-manager><security:authentication-provider><security:user-service><security:user name="user" password="{noop}user"authorities="ROLE_USER" /><security:user name="admin" password="{noop}admin"authorities="ROLE_USER" /></security:user-service></security:authentication-provider></security:authentication-manager>
注意:
存在两个容器,一个是spring容器,一个是springmvc容器。我们将spring security放在父容器spring中。
在applicationContext.xml中配置:
<!--引入springsecurity的配置文件--><import resource="classpath:spring-security.xml"/>
2.1.3Spring Security自定义页面配置
为了保证登录页面不被拦截,需要在spring-security.xml页面中添加参数
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
否则会报错。
自定义页面配置与
<!--配置认证信息--><security:form-login login-page="/login.jsp"login-processing-url="/login"default-target-url="/index.jsp"authentication-failure-url="/failer.jsp"/><!--配置退出登录信息--><security:logout logout-url="/logout"logout-success-url="/login.jsp"/>
为了使用静态资源,还应该进行释放
<!--释放静态资源--><security:http pattern="/css/**" security="none"/><security:http pattern="/img/**" security="none"/><security:http pattern="/plugins/**" security="none"/>
2.2Spring Security的csrf防护机制
CSRF(Cross-site request forgery)跨站请求伪造,是一种难以防范的网络攻击方式。
CsrfFilter.java(GET、HEAD、TRACE、OPTIONS这四个方法直接放行)
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));/** (non-Javadoc)** @see* org.springframework.security.web.util.matcher.RequestMatcher#matches(javax.* servlet.http.HttpServletRequest)*/@Overridepublic boolean matches(HttpServletRequest request) {return !this.allowedMethods.contains(request.getMethod());}}
在login.jsp中添加:
<%@taglib uri="http://www.springframework.org/security/tags" prefix="security"%><form action="${pageContext.request.contextPath}/login" method="post"><security:csrfInput/>...
同理,logout页面也需要进行处理。
<%@taglib uri="http://www.springframework.org/security/tags" prefix="security"%><div class="pull-right">
<%-- <a href="${pageContext.request.contextPath}/logout"--%>
<%-- class="btn btn-default btn-flat">注销</a>--%><form action="${pageContext.request.contextPath}/logout" method="post"><security:csrfInput/><input type="submit" value="注销"></form></div>
也必须携带csrf才能通过CsrfFilter。
2.3Spring Security流程认证分析
UserDetailsService接口
public interface UserDetailsService {// ~ Methods// ========================================================================================================/*** Locates the user based on the username. In the actual implementation, the search* may possibly be case sensitive, or case insensitive depending on how the* implementation instance is configured. In this case, the <code>UserDetails</code>* object that comes back may have a username that is of a different case than what* was actually requested..** @param username the username identifying the user whose data is required.** @return a fully populated user record (never <code>null</code>)** @throws UsernameNotFoundException if the user could not be found or the user has no* GrantedAuthority*/UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
将自己写的UserService接口继承UserDetailService接口。
//org.springframework.security.core.userdetails.User;
//两种构造方式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) {if (((username == null) || "".equals(username)) || (password == null)) {throw new IllegalArgumentException("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));}
可以看到,这个构造方法里多了四个布尔类型的构造参数,其实我们使用的三个构造参数的构造方法里这四个布尔值都被赋值为true,分别表示如下含义:
- boolean enabled 是否可用
- boolean accountNonExpired 账户是否失效
- boolean credentialsNonExpired 秘密是否失效
- boolean accountNonLocked 账户是否锁定
重写loadUserByUsername方法
@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {try{//根据用户名做查询SysUser sysUser =userDao.findByName(username);if(sysUser==null){return null;}//动态决定用户的角色分组List <SimpleGrantedAuthority> authorities = new ArrayList<>();List <SysRole> roles = sysUser.getRoles();for (SysRole role : roles){authorities.add(new SimpleGrantedAuthority(role.getRoleName()));}//{noop}后面的密码,spring security会认为是原文。否则是密文UserDetails userDetails = new User(sysUser.getUsername(), "{noop}"+sysUser.getPassword(), authorities);return userDetails;}catch (Exception e){e.printStackTrace();//认证失败return null;}}
需要实现UserDetails
方法
2.3.1加密认证方式分析
2.3.1.1加盐
根据一定的位置规则对密码以md5的方式进行加密。
package com.itcast.test;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;public class EncodingTest {//加盐加密 加盐//$2a$10$DIrNMaOBys5DhQbqT6KjmuLAljE1Q/.37rDSjTD8MYaq2ycL4Op6C//$2a$10$3Io1IRrHt25OSA8zfSVWK.0qIzq9L.n0gmXMW81IkFAMeSzKvsDuKpublic static void main(String[] args) {BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();System.out.println(passwordEncoder.encode("123"));}
}
spring-security.xml
<!--把加密对象放入到IOC容器中--><bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/><!--设置Spring Security认证用户信息的来源--><!--springsecurity默认的认证必须是加密的,加上{noop}表示不加密认证--><security:authentication-manager><security:authentication-provider user-service-ref="userServiceImpl"><security:password-encoder ref="passwordEncoder"/></security:authentication-provider></security:authentication-manager>
UserServiceImpl
注入对象
@Autowiredprivate BCryptPasswordEncoder passwordEncoder;//保存密码的时候也进行处理@Overridepublic void save(SysUser user) {user.setPassword(passwordEncoder.encode(user.getPassword()));userDao.save(user);}
2.4remember功能基本实现
2.5权限控制
spring-mvc.xml
<!--开启权限控制的注解支持
secured-annotations="enabled" springSecurity内部的权限控制注解开关
pre-post-annotations="enabled" spring指定的权限控制的注解开关
jsr250-annotations="enabled" 开启java250注解支持
--><security:global-method-securitysecured-annotations="enabled"pre-post-annotations="enabled"jsr250-annotations="enabled"/>
为什么应该放在spring-mvc.xml中呢?因为注解是写在controller上的。
2.5.2异常处理方式
方式一:在spring-security.xml配置文件中处理
<!--配置springSecurity--><!--auto-config="true" 表示自动加载springsecurity的配置文件use-expression="true" 表示使用spring的el表达式来配置springsecurity--><security:http auto-config="true" use-expressions="true"><!--省略其它配置--><!--处理403异常--><security:access-denied-handler error-page="/403.jsp"/></security:http>
方式二:在web.xml中处理
<!--处理403异常--><error-page><error-code>403</error-code><location>/403.jsp</location></error-page><!--处理404异常--><error-page><error-code>404</error-code><location>/404.jsp</location></error-page>
方式三:HandlerControllerException.java
package com.itheima.controller.advice;import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@Component
public class HandlerControllerException implements HandlerExceptionResolver {/*** @param httpServletRequest* @param httpServletResponse* @param o 出现异常的对象* @param e 出现异常的信息* @return ModelAndView* **/@Overridepublic ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {ModelAndView mv = new ModelAndView();//将异常信息放入request域中,基本不用mv.addObject("errorMsg", e.getMessage());//指定不同异常跳转的页面if (e instanceof AccessDeniedException){//forward:地址栏不变//redirect:地址栏变化mv.setViewName("redirect:/403.jsp");}else{mv.setViewName("redirect:/500.jsp");}return mv;}
}
方式四:HandlerControllerAdvice.java
package com.itheima.controller.advice;import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;@ControllerAdvice
public class HandlerControllerAdvice {@ExceptionHandler(AccessDeniedException.class)public String handlerException (){return "redirect:/403.jsp";}@ExceptionHandler(RuntimeException.class)public String runtimeHandlerException (){return "redirect:/500.jsp";}}
2.6 Spring Security与Spring Boot整合使用
2.6.2分布式访问
分布式认证概念说明:分布式认证,即常说的单点登录,简称SSO,指的是在多应用系统的项目中,用户只需要登录一次,就可以访问所有互相信任的应用系统。
分布式认证流程图:
首先需要明确的是,在分布式项目中,每台服务器都有各自独立地session,而这些session之间无法直接共享资源,因此,session不能被作为单点登录的解决方案。
总结一下,单点登录的实现分为两大环节:
- 用户认证:这一环节主要是用户向认证服务器发起认证请求,认证服务器给用户返回一个成功的令牌token,主要在认证服务器中完成,即图中的A系统,注意A系统只能有一个。
- 身份校验:这一环节是用户携带token去访问其它服务器时,在其它服务器中要对token的真伪进行校验,主要在资源服务器中完成,即图中的B系统,这里的B系统可以有多个。
2.6.3JWT介绍
概念说明
分布式认证流程中,中间起最关键作用的就是token,token的安全与否,直接关系到系统的健壮性,这里选择使用JWT来实现token的生成和校验。
JWT,全称为JSON Web Token,可以生成token,也可以解析检验token。
JWT生成的token由三部分组成:
- 头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。
- 载荷:将头部与载荷分别采用base64编码之后,用"."相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。
2.6.4非对称加密RSA介绍
- 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以发给信任客户端
- 私钥加密,持有私钥或者公钥才可以解密
- 公钥加密,持有私钥才可以解密
- 优点:安全,难以破解
- 缺点:算法耗时
2.6.5SpringSecurity+JWT+RSA分布式认证思路分析
集中式认证流程
- 用户认证:
使用UsernamePasswordAuthenticationFilter
过滤器中attemptAuthentication
方法实现认证功能,该过滤器父类中successfulAuthentication
方法实现认证成功后的操作。 - 身份校验:
使用BasicAuthenticationFilter过滤器中doFilterInternal方法验证是否登录,以决定能否进入后续过滤器。
分布式认证流程
用户认证:
分布式项目,多数是前后端分离的架构设计,我们要满足可以接受异步post的认证请求参数,需要修改UsernamePasswordAuthenticationFilter
过滤器中attemptAuthentication
方法,让其能够接收请求体。
另外,默认successfulAuthentication
方法在认证通过后,是把用户信息直接放入session就完事了,现在我们需要修改这个方法,在认证通过后生成token并返回给用户。身份校验:
原来BasicAuthenticationFilter
过滤器中doFilterInternal
方法校验用户是否登录,就是看session中是否有用户信息,我们需要修改为,验证用户携带的token是否合法,并解析出用户信息,交给SpringSecurity,以便于后续的授权功能可以正常使用。
2.6.6OAuth2介绍
OAuth是Open Authorization的简写。OAuth协议为用户资源的授权提供了安全地、开放而又简易的标准。与以往的授权不同之处在于,OAuth的授权不会使得第三方触及到用户的账号信息(如用户名和密码),即第三方无需使用用户的用户名和密码就可以申请获得该用户资源的授权,因此OAuth是安全的。
使用场景
假设,A网站是一个打印照片的网站,B网站是一个存储照片的网站,二者毫无关联。如果一个用户想使用A网站打印自己存储在B网站的照片,那么A网站就需要使用B网站的照片资源才行。
按照传统的思考模式,我们需要A网站具有登录B网站的用户名和密码才行,但是,有了OAuth2之后,只需要A网站获取到使用B网站照片资源的一个通行令牌即可!这个令牌无需具备操作B网站所有资源的权限,也无需永久有效,只要满足A网站打印照片需求即可。
和单点登录不同??
2.6.6.1授权码模式
流程
说明:【A服务客户端】需要用到【B服务资源服务】中的资源
第一步:【A服务客户端】将用户自动导航到【B服务认证服务】,这一步用户需要提供一个回调地址,以备【B服务认证服务】返回授权码使用。
第二步:用户点击授权按钮表示让【A服务客户端】使用【B服务资源服务】,这一步需要用户登录B服务,也就是说用户要事先具有B服务的使用权限。
第三步:【B服务认证服务】生产授权码,授权码将通过第一步提供的回调地址,返回给【A服务客户端】。注意这个授权码并非通行【B服务资源服务】的通行凭证。
第四步:【A服务认证服务】携带上一步得到的授权码向【B服务认证服务】发送请求,获取通行凭证token。
第五步:【B服务认证服务】给【A服务认证服务】返回令牌token和更新令牌refresh token。使用场景
授权码模式是OAuth2中最安全最完善的一种模式,应用场景最广泛,可以实现服务之间的调用,场景的微信、QQ等第三方登录也是这种方式实现。
2.6.6.2简化模式(implicit)
- 流程
说明:简化模式中没有【A服务认证服务】这一部分,全部有【A服务客户端】与B服务交互,整个过程不再有授权码,token直接暴露在浏览器。
第一步:【A服务客户端】将用户自动导航到【B服务认证服务】,这一步需要提供一个回调地址,以备【B服务认证服务】返回token使用,还会携带一个【A服务认证客户端】的状态标识state。
第二步:用户点击授权按钮表示让【A服务客户端】使用【B服务资源服务】,这一步需要用户登录B服务,也就是说用户要事先具有B服务的使用权限。
第三步:【B服务认证服务】生成通行令牌token,token将通过第一步提供的回调地址,返回给【A服务客户端】。 - 使用场景
适用于A服务没有服务器的情况,比如:纯手机小程序、JavaScirpt语言实现的网页插件等。
2.6.6.3密码模式(resource owner password credentials)
- 流程
第一步:直接告诉【A服务客户端】自己的【B服务认证服务】的用户名和密码
第二步:【A服务客户端】携带【B服务认证服务】的用户名和密码向【B服务认证服务】发起请求获取token。
第三步:【B服务认证服务】给【A服务客户端】颁发token。 - 使用场景
此种模式虽然简单,但是用户将B服务的用户名和密码暴露给了A服务,需要两个服务信任度非常高才能使用。
2.6.6.4客户端模式(client credentials)
- 流程
说明:这种模式其实已经不太属于OAuth2的范畴了。A服务完全脱离用户,以自己的身份去向B服务索取token。换言之,用户无需具备B服务的使用权也可以。完全是A服务和B服务内部的交互,与用户无关了。
第一步:A服务向B服务索取token
第二步:B服务返回token给A服务 - 使用场景
A服务本身只需要B服务资源,与用户无关。
2.7 Spring Security注解模式
2.7.1@EnableWebSecurity和@EnableGlobalMethodSecurity
1、通过@EnableWebSecurity
注解开启Spring Security的功能。
Spring Security默认是禁用注解的,要开启注解的话,需要在继承WebSecurityConfigureAdapter
的类上(一般是用来配置Spring security相关信息的)加@EnableGlobalMethodSecurity
注解。
示例如下:
WebSecurityConfig.java
package com.itheima.config;import com.itheima.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Configuration
@EnableWebSecurity
//开启方法级授权方式
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {// 认证用户的来源[内存或者数据库]@Autowiredprivate UserService userService;@Beanpublic BCryptPasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}public void configure(AuthenticationManagerBuilder auth) throws Exception {// auth.inMemoryAuthentication()
// .withUser("user")
// .password("{noop}123")
// .roles("USER");auth.userDetailsService(userService).passwordEncoder(passwordEncoder());}// 配置springSecurity相关信息public void configure(HttpSecurity http) throws Exception {//释放静态资源,指定资源拦截规则,指定自定义认证页面,指定退出认证配置,csrf配置http.authorizeRequests().antMatchers("/login.jsp", "failer.jsp", "/css/**", "/img/**", "/plugins/**").permitAll().antMatchers("/product").hasAnyRole("USER").anyRequest().authenticated().and().formLogin().loginPage("/login.jsp").loginProcessingUrl("/login").successForwardUrl("/index.jsp").failureForwardUrl("/failer.jsp").and().logout().logoutSuccessUrl("/logout").invalidateHttpSession(true).logoutSuccessUrl("/login.jsp").and().csrf().disable();}
}
使用@EnableGlobalMethodSecurity
这个注解,可以开启security的注解。
2、在Controller类上使用权限注解
package com.itheima.controller;import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;@Controller
@RequestMapping("/product")
public class ProductController {@Secured("ROLE_PRODUCT")@RequestMapping// 返回数据,而不是跳转页面// @ResponseBodypublic String findAll(){return "product_list";}
}
3、@EnableGlobalMethodSecurity详解
@EnableGlobalMethodSecurity(securedEnabled=true)
开启@Secured
注解过滤权限
@EnableGlobalMethodSecurity(jsr250Enabled=true)
开启@RolesAllowed
注解过滤权限
@EnableGlobalMethodSecurity(prePostEnabled=true)
使用表达式时间方法级别的安全性,4个注解可用
- @PreAuthorize:在方法调用之前,基于表达式的计算结果来限制对方法的访问
- @PostAuthorize:允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
- @PostFilter:允许方法调用,但必须按照表达式来过滤方法的结果
- @PreFilter:允许方法调用,但必须在进入方法之前过滤输入值
3.SpringSecurity-web权限方案
web权限方案:
(1)认证
(2)授权
1、设置登录的用户名和密码
- 第一种方式:通过配置文件
- 第二种方式:通过配置类
- 第三种方式:自定义编写实现类
3.1设置登录系统的账号、密码
方式一:在application.properties
server.port=8111
spring.security.user.name=atguigu
spring.security.user.password=atguigu
方式二:编写类实现接口
方式三:自定义实现类设置
- 第一步 创建配置类,设置使用哪个
userDetailService
实现类 - 第二步 编写实现类,返回User对象,User对象有用户名密码和操作权限
查询数据库完成用户认证
- 整合MyBatisPlus完成数据库操作
- 第一步:引入相关依赖
- 第二步:创建数据库和数据库表
- 第三步:创建users表对应实体类
- 第四步:整合mp,创建接口,继承mp的接口
- 第五步:在MyUserDetailService调用mapper里面的方法查询数据库进行用户认证
4.SpringSecurity微服务权限方案
1、什么是微服务
2、微服务认证和授权实现过程
3、完成基于SpringSecurity认证授权案例
4.2数据模型介绍
1、权限管理数据模型
2、案例涉及技术说明
4.3使用技术说明
5.SpringSecurity源码剖析
5.1认证流程
UsernamePasswordAuthenticationFilter
:
(1)查看过滤器的父类 AbstractAuthenticationProcessingFilter
- 第一步:过滤的方法,判断提交方式是否POST提交
- 第二步:调用子类的方法进行身份认证,认证成功之后,把认证信息封装到对象里面
- 第三步:session策略处理
- 第四步 1 认证失败抛出异常,执行认证失败的方法
- 第四步 2 认证成功,调用认证成功的方法
(2)上面第二步 调用子类的方法进行认证过程,查看源码
构造方法,要求是POST请求和/login
登录方式
UsernamePasswordAuthenticationFilter
类
public Authentication attemptAuthentication(HttpServletRequest request,
方法
方法第一步: 判断是否POST提交
方法第二步:获取表单提交的用户名和密码
方法第三步:使用获取数据,构造成对象,标记未认证,把请求一些属性信息设置到对象里面
调用方法进行身份认证(调用UserDetailsService)
(3)查看UsernamePasswordAuthenticationToken
的构建过程
(4)查看ProviderManager源码,认证实现
(5)认证成功/认证失败
5.2权限访问流程
ExceptionTranslationFilter
:
FilterSecurityInterceptor
:
5.5SpringSecuirty请求间共享认证信息
一般认证成功后的用户信息是通过Session在多个请求之间共享,那么Spring Security中是如何实现将已认证的用户信息对象Authentication
与Session
绑定的进行具体分析。
请求间认证共享
(1)把认证信息对象,封装到SecurityContext里面,存入SecurityContextHolder里面
(2)SecurityContext对象
(3)SecurityContextHolder
使用ThreadLocal进行操作
Spring Security使用记录相关推荐
- Spring Security相关
本文记录Spring Security相关的知识 文章目录 Spring Security相关 Spring Security相关 记录spring Security相关的知识,spring Secu ...
- 后端架构token授权认证机制:spring security JSON Web Token(JWT)简例
后端架构token授权认证机制:spring security JSON Web Token(JWT)简例 在基于token的客户端-服务器端认证授权以前,前端到服务器端的认证-授权通常是基于sess ...
- spring security 整合sso全记录
spring security 整合sso全记录 介绍一下我司的sso流程 app security 整合sso的思路 要解决的问题 上代码 介绍一下我司的sso流程 我司的sso流程: app在ss ...
- 【Spring Boot】Spring Boot 2.x + Spring Security OAuth2 2.3.3 出现 bad client credentials 错误的踩坑记录
环境: spring boot 2.0.4.RELEASE spring security oauth 2.3.3.RELEASE OAuth2的配置 @Configuration @EnableAu ...
- Spring Security 源码分析:Spring Security 授权过程
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring I ...
- 7.Spring Security 退出登录
Spring Security默认的退出登录URL为/logout,退出登录后,Spring Security会做如下处理: 是当前的Sesion失效: 清除与当前用户关联的RememberMe记录: ...
- Spring Security源码分析八:Spring Security 退出
为什么80%的码农都做不了架构师?>>> Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spr ...
- Spring Security and Shiro
Spring Security 看了第一篇的一部分,后几个还没看: http://www.cnblogs.com/javay/p/5822879.html http://blog.csdn.net/ ...
- spring security oauth rce (cve-2016-4977) 漏洞分析
0x00 漏洞概述 1.漏洞简介 Spring Security OAuth是为Spring框架提供安全认证支持的一个模块,在7月5日其维护者发布了这样一个升级公告,主要说明在用户使用Whitelab ...
最新文章
- android调用web接口,Android调用webservice 接口
- all the input arrays must have same number of dimensions
- Redis简单案例(二) 网站最近的访问用户
- Python4:DataStructure
- Linux popen和pclose启动shell命令的问题思考
- OpenCV学习笔记(十三):霍夫变换:HoughLines(),HoughLinesP(),HoughCircles( )
- 选择嵌套_还不会if函数的嵌套判断,学会这方法,就跟复制粘贴一样简单
- canvas绘制竖排的数字_大佬教你用Python Tkinter实现数字猜谜小游戏
- Linux——vim编辑器详解
- 安卓数据读写全解:SharedPreferences公共数据的读写,SQLiteDatabase数据库,mysql数据库
- jdk8 mysql安装教程_Linux系统:centos7下安装Jdk8、Tomcat8.5、MySQL5.7环境
- 普通摄像头的数据输出格式YUV与mjpeg之间联系、DCT离散余弦变换去噪跟压缩(待补充)
- 语言中出现蘌ress_语言障碍、语言异常及语言发育迟缓的异同
- 对于计算机老师的教学评语,电脑教师的自我评价
- 算法竞赛资料整理分享
- windows系统C++获取当前电脑电池信息
- 5月17号软件资讯更新合集....
- UNRAID挂载exFat格式的USB磁盘后续(自动挂载)
- JDK1.8之前造成HashMap死链问题
- 实用的一些网站 合集