这篇文章主要围绕配置SpringSecurity前台后台登录处理和springsecurity配置展开,旨在为您提供一份详细的参考资料。我们将全面介绍配置SpringSecurity前台后台登录处理的
这篇文章主要围绕配置SpringSecurity前台后台登录处理和springsecurity 配置展开,旨在为您提供一份详细的参考资料。我们将全面介绍配置SpringSecurity前台后台登录处理的优缺点,解答springsecurity 配置的相关问题,同时也会为您带来16.SpringSecurity-短信登录配置及重构、23.SpringSecurity-SpringSecurityOAuth简介、25.SpringSecurity-SpringSecurityOAuth核心源码解析、33.SpringSecurity-SpringSecurity Oauth授权源码解读的实用方法。
本文目录一览:- 配置SpringSecurity前台后台登录处理(springsecurity 配置)
- 16.SpringSecurity-短信登录配置及重构
- 23.SpringSecurity-SpringSecurityOAuth简介
- 25.SpringSecurity-SpringSecurityOAuth核心源码解析
- 33.SpringSecurity-SpringSecurity Oauth授权源码解读
配置SpringSecurity前台后台登录处理(springsecurity 配置)
下面的代码配置为后台登录、前台登录验证处理。但是无法保存两个登录状态,要么是登录后台,要么是登录前台<!--<debug/>-->
<beans:bean id="userBCryptEncoder">
</beans:bean>
<beans:bean id="adminUserDetailsService">
<beans:property name="jdbcTemplate" ref="jdbcTemplate"/>
</beans:bean>
<authentication-manager id="adminAuthenticationManager" alias="adminAuthenticationManager">
<authentication-provider user-service-ref="adminUserDetailsService">
<password-encoder ref="userBCryptEncoder"/>
</authentication-provider>
</authentication-manager>
<!-- 后台登录页面-->
<beans:bean id="adminLoginEntryPoint">
<beans:constructor-arg name="loginFormUrl" value="/admin/login"/>
<beans:property name="forceHttps" value="true"/>
<beans:property name="useForward" value="true"/>
</beans:bean>
<beans:bean id="adminAccessDecisionManager">
<beans:constructor-arg name="decisionVoters" >
<beans:list>
<beans:bean/>
<beans:bean/>
<beans:bean/>
</beans:list>
</beans:constructor-arg>
</beans:bean>
<!-- 后台用户安全配置-->
<http pattern="/admin/**" authentication-manager-ref="adminAuthenticationManager"
access-decision-manager-ref="adminAccessDecisionManager"
use-expressions="true"
access-denied-page="/admin/login">
<remember-me key="fengyunhe" user-service-ref="adminUserDetailsService"/>
<intercept-url pattern="/admin/login" access="permitAll()" requires-channel="https"/>
<intercept-url pattern="/admin/login_submit" access="permitAll()" requires-channel="https"/>
<form-login login-page="/admin/login"
default-target-url="/admin/"
authentication-failure-url="/admin/login"
login-processing-url="/admin/login_submit"
always-use-default-target="true"
/>
<logout logout-url="/admin/logout" invalidate-session="true" delete-cookies="JSESSIONID"
logout-success-url="/admin/login"/>
<intercept-url pattern="/admin/**" access="hasRole(''ROLE_ADMIN'')" requires-channel="https"/>
<session-management invalid-session-url="/admin/login" session-authentication-error-url="/admin/login"
session-fixation-protection="migrateSession" >
<!--<concurrency-control max-sessions="1" error-if-maximum-exceeded="true"/>-->
</session-management>
</http>
<!-- 下面是会员的登录和权限控制-->
<authentication-manager id="memberAuthenticationManager" alias="memberAuthenticationManager">
<authentication-provider user-service-ref="memberUserDetailsService">
<password-encoder ref="userBCryptEncoder"/>
</authentication-provider>
</authentication-manager>
<beans:bean id="memberLoginEntryPoint">
<beans:constructor-arg name="loginFormUrl" value="/member/login"/>
<beans:property name="forceHttps" value="true"/>
<beans:property name="useForward" value="true"/>
</beans:bean>
<beans:bean id="memberUserDetailsService">
<beans:property name="jdbcTemplate" ref="jdbcTemplate"/>
</beans:bean>
<beans:bean id="memberAccessDecisionManager">
<beans:constructor-arg name="decisionVoters" >
<beans:list>
<beans:bean/>
<beans:bean/>
<beans:bean/>
</beans:list>
</beans:constructor-arg>
</beans:bean>
<http pattern="/member/**" authentication-manager-ref="memberAuthenticationManager"
access-decision-manager-ref="memberAccessDecisionManager"
use-expressions="true"
access-denied-page="/member/login" >
<remember-me key="fengyunhe" user-service-ref="memberUserDetailsService"/>
<intercept-url pattern="/member/login" access="permitAll()" requires-channel="https"/>
<intercept-url pattern="/member/login_submit" access="permitAll()" requires-channel="https" />
<form-login login-page="/member/login"
default-target-url="/member/"
authentication-failure-url="/member/login"
login-processing-url="/member/login_submit"
username-parameter="username"
password-parameter="password"
always-use-default-target="true"
/>
<logout logout-url="/member/logout" invalidate-session="true" delete-cookies="JSESSIONID"
logout-success-url="/member/login"/>
<intercept-url pattern="/member/**" access="hasRole(''ROLE_MEMBER'')" requires-channel="http"/>
<session-management session-fixation-protection="none">
<!--<concurrency-control max-sessions="1" error-if-maximum-exceeded="true"/>-->
</session-management>
</http>
注意,如果关闭固定session攻击防御则只可能配置session-fixation-protection="none",其他的属性不能带,否则还是会开启固定session攻击防御。开启后会对用https登录切回http的这种方式造成影响,因为session发生变化,给的sessionId为secure,无法在http下获取到,所以会导致跳回http后还是在登录页面的问题。
tips:
本文由wp2Blog导入,原文链接:http://devonios.com/%e9%85%8d%e7%bd%aespringsecurity%e5%89%8d%e5%8f%b0%e5%90%8e%e5%8f%b0%e7%99%bb%e5%bd%95%e5%a4%84%e7%90%86.html
16.SpringSecurity-短信登录配置及重构
1.短信登录配置及重构
- 短信登录一部分配置主要是配置以下3个组件:
主要是配置:SmsCodeAuthenticationFilter和SmsCodeAuthenticationProvider;
SmsCodeAuthenticationToken是封装数据,是不用配置的。
- 另一部分配置是短信过滤器配置:SmsCodeFilter
1.1 过滤器链上组件配置
我们在spring-security-core项目创建一个SmsCodeAuthenticationSecurityConfig;我们没有向之前WebSecurityConfig一样配置在spring-security-web里面;原因是因为:短信校验模块这段配置后面我们既要在web浏览器端使用还要在App端使用。所以我们单独提出一个类。
@Configuration
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private UserDetailsService myUserDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
/* super.configure(builder);*/
//1.按照过滤器链路:配置SmsCodeAutnenticationFilter-->配置让其加入到AuthenticationManager中 配置其成功处理 失败处理的Handler
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
//2.配置SmsCodeAuthenticationProvider-->添加UserDetailService
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(myUserDetailsService);
//3.http配置:配置SmsCodeAutnenticationFilter执行位置--->在UsernamePasswordAuthenticationFilter之后
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
1.2 验证码过滤器配置
在Spring-security-web里面配置:SmsCodeFilter在UsernamePasswordFilter前面
1.3 SmsCodeAuthenticationSecurityConfig配置到WebSecurityConfig
我们把spring-security-core里面的SmsCodeAuthenticationSecurityConfig也配置导入到spring-security-web下面的:WebSecurityConfig配置中:
1.4 测试查看效果
http://127.0.0.1:8088/login.html
- 点击短信登录的登录后返回:
- 点击发送验证码
向手机:13012345678 发送短信验证码:711828
- 我们输入短信验证码,然后登录
得出结果:
2.短信登录重构
现在为止,两种登录方式都已经开发完了。
- 他们都可以在一个系统里面同时生效。
- 底层都是Spring Security,采用的是同一种机制和流程,但是认证的逻辑是不同的。
目前代码重构主要是做两方面内容:
消除重复: 短信验证码和图形验证码里面重复的代码,因为我们是做的拷贝粘铁,然后做了一些简单修改。实现的短信验证码校验功能。 重复的代码是代码坏味道 里面最常见和最严重的。他使得你的代码难以维护,虽然开发时候挺快的,一旦业务逻辑变化,修改代码时候,你可能需要把复制粘贴地方都要修改。重复一个功能模块、小到配置项:比如:在WebSecurityConfig里面:
WebSecurityController里面:
都有相同的代码:/authentication/require
以上修改的时候,你同样需要改变一下,改了一个地方,另一个地方忘记改了,也会出问题。
- 配置抽象:就像我们把短信验证码授权安全配置一样:SmsCodeAuthenticationSecurityConfig 单独抽象到一个类里面。所以这里我们把验证码统一都抽象出来,短信验证码、图形验证码。短信配置的话 也放到一个配置里面去。
- 不管在app端,还是浏览器端应用的一些配置我们都抽取到spring-security-core里面
- 上面基本配置都封装成各个类的话,这样的话在WebSecurityConfig里面只剩下其特有的配置代码;比如:remeberMe就是其特有代码。remeberMe实现是基于往浏览器里面写cookie实现。
- App里面我们会单独写一个AppSecurityConfig配置代码
- 这样的话AppSecurityConfig、WebSecurityConfig可以使用如下引用方法去引用:
AppSecurityConfig、WebSecurityConfig需要支持认证登录的话都得配置。
2.1 验证码校验过滤器合并成一个
验证码校验过滤器合并成一个ValidateCodeFilter;其启动时候会根据系统的配置:
读取到一个map里面去:
/**
* 存放所有需要校验验证码的url
*/
private Map<String, ValidateCodeType> urlMap = new HashMap<>();
里面存放着什么样的验证码使用什么样的处理器。
根据依赖查找查找到对应的校验码处理器:
2.2 密码相关配置抽象
定义一个抽象类:AbstractChannelSecurityConfig 把密码相关配置放在里面。
/*
* @author yexinming
* @date 2020/2/27
*/
public class AbstractChannelSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
protected AuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
protected AuthenticationFailureHandler myAuthenticationFailureHandler;
protected void applyPasswordAuthenticationConfig(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM)
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler);
}
}
2.3 所有模块配置抽象出来
2.4 字符串抽象
将常用配置项配置到SecurityContants中:
public interface SecurityConstants {
/**
* 默认的处理验证码的url前缀
*/
public static final String DEFAULT_VALIDATE_CODE_URL_PREFIX = "/code";
/**
* 当请求需要身份认证时,默认跳转的url
*
* @see SecurityController
*/
public static final String DEFAULT_UNAUTHENTICATION_URL = "/authentication/require";
/**
* 默认的用户名密码登录请求处理url
*/
public static final String DEFAULT_LOGIN_PROCESSING_URL_FORM = "/authentication/form";
/**
* 默认的手机验证码登录请求处理url
*/
public static final String DEFAULT_LOGIN_PROCESSING_URL_MOBILE = "/authentication/mobile";
/**
* 默认登录页面
*
* @see SecurityController
*/
public static final String DEFAULT_LOGIN_PAGE_URL = "/login.html";
/**
* 验证图片验证码时,http请求中默认的携带图片验证码信息的参数的名称
*/
public static final String DEFAULT_PARAMETER_NAME_CODE_IMAGE = "imageCode";
/**
* 验证短信验证码时,http请求中默认的携带短信验证码信息的参数的名称
*/
public static final String DEFAULT_PARAMETER_NAME_CODE_SMS = "smsCode";
/**
* 发送短信验证码 或 验证短信验证码时,传递手机号的参数的名称
*/
public static final String DEFAULT_PARAMETER_NAME_MOBILE = "mobile";
/**
* session失效默认的跳转地址
*/
public static final String DEFAULT_SESSION_INVALID_URL = "/session/invalid";
}
23.SpringSecurity-SpringSecurityOAuth简介
前言
Spring Security OAuth开发App认证框架
基于session实现的保存用户登录信息:
- 我们现在用户登录成功之后,用户信息都是存在后端Session的,用户通过浏览器每次去访问的时候,每一次服务器都会去检查浏览器的cookie里面是否存在JSESSIONID,不存在jsessionid,我们就会去服务器创建一个session,然后把新创建的sessionid传回到浏览器cookie中。这样每次用户通过浏览器发请求的时候,我们会根据JSESSIONID找到session,然后找到对应的用户信息。
前后端分离架构:
- 移动互联网到来后,新的访问渠道出现了:APP,除了这种访问渠道外,应用的部署方式也在不断演进。现在比较流行的方式就是前后端分离。 前后端分离时候,前端资源不跟后端资源在同一个服务器。而是单独部署到一个Web Server上(比如:node.js)。 采用这种前后端分离架构的时候,用户访问的是WebServer。页面渲染和ajax处理都是这个Web Server来处理的。 用户请求发送到Web Server时候,Web Server再将这个请求转发到Application Server上面。
新架构变化带来的问题:
- 用户不是直接通过浏览器访问我们的Application Server应用,而是通过App第三方应用,或者Web Server;访问我们的内部应用,不是浏览器了,而是其他第三方应用。使用我们的cookie和session就会出现问题。
- 这种架构下:能不能用session和cookie的方式呢?答案是可以的。只要App和Web Server允许你去操作cookie,Session,那么就可以用上面这种方式的。但是在这种架构下cookie和session会有一些问题:
a.开发繁琐(针对于cookie这种功能是浏览器已经内建好的,我们不需要针对cookie写很多代码;但是针对于新架构:App在关闭打开时候,需要在代码里面去实例化这种http客户端,然后发送请求,里面的cookie都是空的,都是初始化的一种状态。这个时候,你就要自己去维护一个cookie的存储)
b.安全性和客户体验差:因为基于cookie和session这种方式,实际上他的这种验证工作都是服务器自己做的。其实也没有说很么验证工作,就是你传过来的cookie里有sessionid,那么我直接就从session里面拿东西。就认为你登录了,没有再去验证 其他东西,那么这种情况下就会导致:比如你的jsessionid被别人知道了,那么他用这个jsessionid放到cookie里面就可以获取用户信息;可能有些人为了解决这种情况;想把session时间设置短一些。然后让其尽快失效掉。那么就会带来另一个问题:session频繁失效就意味着你用户需要频繁去登录。就会带来用户体验差,如果设置长,丢了,用户信息就很可能丢失。
c.有些前端技术不支持cookie,如:小程序。
所以当程序访问者不再是浏览器,而是一些应用时候,我们应该用另一种方式去存储用户信息。这种方式就是我们常说的方式token,原理和session差不多 都是给用户返回一个信息。session方式下是往浏览器里面cookie里面去写一个jsessionid,而用令牌的方式,我是直接发送一个token,用户每次访问的时候,也是带着这个token,用户信息不再存储在session里面,而是根据用户每次请求的令牌来判断:用户是谁?用户有什么权限?用户能干啥。用户认证和授权不再基于session了,而是token令牌。令牌会让我们前面说的问题得到解决。
a.开发繁琐(用户每次请求时候携带的是token,是存放在http的参数;和携带其他参数一样就是一个字符串,token不需要像cookie那样专门做一个复杂开发)
b.安全性和客户体验差:(session这种方式是服务器自己完成的,我们没法干预,但是针对于token这种方式:他怎样生成?里面包含了什么信息?我们怎样校验,都是我们自己可以控制的;安全性:我们可以设置token的过期时间短一些,可以使用token刷新方式,变换token;用户体验:使用token刷新方式,变换token,让用户在没有感知情况下刷新令牌,避免让用户重新重复登录;保证token安全性)。
c.有些前端技术不支持cookie,如:小程序。
上面那种方式就是前面说的oauth协议,我们把App和Web Server当做是应用第三方。每次校验都是通过token实现。
内容
1.1 SpringSocial和SpringSecurityOauth区别
- Spring Social是客户端封装,Spring Security Oauth是服务端封装
- Spring Social开发第三方登录时候,实际上封装了第三方应用也就是client他所要做的大部分事情。拿着Spring Social我们可以很快开发一个第三方应用角色去连接我们服务提供商。
Spring Security Oauth则是封装了服务提供商需要完成的绝大部分的行为;使用Spring Security OAuth,我们就能很快搭建一个服务提供商的程序来。然后往外发送令牌、验收令牌。
1.2 SpringSecurityOauth简介
上面绿色的快都是SpringSecurity给我们实现好了的,红色的快是我们需要自己实现的 ;我们实现的这块:资源是标准的服务是不用改的。我们要改的是自定义的认证方式
- 要实现Spring Security Oauth服务提供商的内容,其实就是需要实现两个服务器;也就是我们之前介绍的认证服务器(Authorization Server)和 资源服务器(Resource Server)
- 在认证服务器里面要做的就是4种授权模式;通过这4种模式来区分用户的身份,以及他所拥有的权限。Spring Security Oauth其实已经把这种模式帮我们实现了。这4种模式实现后,通过用户名/密码.我们知道用户信息之后,我们第二步要做的就是根据我们的这些信息生成Token令牌 即Token的生成存储。后面会根据这个令牌拿去用户信息做检验,所以Token不但生成也得存储。
- 资源服务器就是要保障我们的资源:资源其实就是我们在应用里面写的这些rest服务,怎样来保护这些服务呢?按照目前模式我们使用的是Spring Security过滤器链。他是在我们资源之前加一个过滤器链?那么Spring Security OAuth如何实现资源服务器这样一个功能的呢?他就是在SpringSecurity过滤器链上面添加一个新的过滤器:OAuth2AuthenticationProcessingFilter(从请求中拿到你发送出去的token,然后根据你配置的存储策略去存储里面找到用户对应信息,根据这个用户信息是否存在?是否有权限等一系列判断来决定他是否能访问到你决定的资源,从而实现了资源服务器的功能)
- 我们其实不希望让用户走这4种标准的授权模式的。比如手机号和短信验证码的登录方式。其实跟我们标准的这4中授权模式是搭不上的。标准的这4中授权模式是没有让你输入手机号、输入短信的。然后我们就发一个token给你。我们需要做自定义的认证方式,让这种自定义的认证方式也可以嫁接到认证服务器上去。用户通过这种自定义的认证方式:用户名/密码、手机号、短信、还有第三方的认证之后调用token机制生成token发送到用户。
1.3 接下来需要做的是
- 实现一个标准的OAuth2协议中Provider角色的主要功能:自定义认证
- 重构之前的3种认证方式代码,使其支持token
- 高级特性(JWT,单点登录)
25.SpringSecurity-SpringSecurityOAuth核心源码解析
前言
- 之前我们已经完成了上面的服务提供商中:认证服务器、资源服务器的开发。 实际上代码非常简单,就是使用Spring Security OAuth的注解。 有了这些注解后我们已经可以按照标准的OAuth协议往外边发访问令牌access_token。上节演示了如何用授权码模式,密码模式获取access_token。然后拿着这些令牌去访问我们的资源服务器中的资源。
- 以上标准流程做完之后,我们接下来需要做的就是把我们之前做的三种模式(短信登录、QQ登录、微信登录)嫁接到标准模式中去。让我们在这三种模式认证通过之后。也可以返回access_token。那么为了实现这个目的,我们需要实现上面这个源码。
内容
1.Spring Security Oauth核心源码
- 上图中:绿色方块表示实体类,蓝色方块表示接口,括号中为真正实现类。
- TokenEndpoint是程序入口点,可以理解成一个controller,他会处理令牌请求。上一节中我们用授权码模式、密码模式时候,请求的url都是一致的,他是通过grant\_type来告知我们使用的是哪种请求模式。他处理/oauth/token请求。当收到令牌请求时候,TokenEndPoint会调用我们的ClientDetailsService
- ClientDetailsService区别于UserDetailsService;UserDetailsService是读取用户信息的,ClientDetailsService用来读取第三方应用信息的。我们之前发送请求时候在Header里面都会携带clientId和clientSecret来告诉是哪个应用请求授权,这个ClientDetailsService就会根据传过来的clientId和clientSecret读取响应的client配置信息。这些配置信息都会读取到ClientDetails这个对象里面去。
- ClientDetails:这里面封装的是第三方应用的信息,然后TokenEndpoint还会创建一个TokenRequest对象。
- TokenRequest:封装了我们请求的其他参数信息,比如:grant_type;如果是密码模式的话,我们的用户名、密码是什么?同时也会把ClientDetails信息放到TokenRequest里面去。因为第三方应用信息 也是令牌请求一部分。利用TokenRequest会去调用我们的TokenGranter(令牌授权者)接口,这个接口后面其实封装了我们的4种授权模式的不同实现。这个接口里面会根据你接口传上来的grant_type去挑选一个自己的实现去执行令牌的生成。不管是哪种实现,生成过程中都会产生2种东西:OAuth2Request和Authentication。
- OAuth2Request其实就是之前ClientDetails和TokenRequest信息的整合。
- Authentication接口封装了当前授权用户的一些信息。谁在做一些授权,授权用户信息就是在Authentication接口里面。它里面的信息是我们通过UserDetailsService读出来的。
- OAuth2Authentication:OAuth2Request和Authentication整合形成。它里面包含了现在是哪一个第三方应用在请求哪个用户在授权,然后用的授权模式是什么?授权参数是什么等?然后会传递给接口:AuthorizationServerTokenServices
- AuthorizationServerTokenServices拿到我们OAuth2Authentication之后,他会最终生成一个OAuth2AccessToken令牌。AuthorizationServerTokenServices的默认实现的:DefaultTokenServices里面含有两个接口的引用:TokenStore和TokenEnhancer
- TokenStore用来处理令牌存储。
- TokenEnhancer令牌生成器,当我们令牌生成之后,我们可以自定义去改造令牌。
2.源码追踪
我们使用密码模式追踪。因为授权码模式需要两步(第一步获取授权码,然后拿着授权码去获取token),测试起来比较麻烦。
然后生成TokenRequest:
//根据请求参数和客户端信息创建tokenRequest
TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter.");
} else {
String clientId = this.getClientId(principal);
//1.获取三方授权信息
ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);
//2.根据三方授权信息和参数生成TokenRequest
TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
//3.参数校验
if (clientId != null && !clientId.equals("") && !clientId.equals(tokenRequest.getClientId())) {
throw new InvalidClientException("Given client ID does not match authenticated client");
} else {
if (authenticatedClient != null) {
this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
//简化模式:是在用户授权时候直接返回令牌,不会存在请求令牌服务被调用
} else if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
} else {
//判断是否是授权码模式的请求:我们需要重新设置scope
if (this.isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) {
this.logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.emptySet());
}
//如果是刷新令牌的请求,我们需要重新设置scope
if (this.isRefreshTokenRequest(parameters)) {
tokenRequest.setScope(OAuth2Utils.parseParameterList((String)parameters.get("scope")));
}
//4.根据TokenGranter创建 OAuth2AccessToken
OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
} else {
return this.getResponse(token);
}
}
}
}
}
我们进入TokenGranter其实现类:CompositeTokenGranter里面:
里面会存在包含refresh\_token的5种模式,一次遍历找到对应目前人的授权。
然后根据传入的grantType参数,找到对应的授权类型,生成: ,然后返回回去,产生最终令牌:
密码模式的实现:
然后进入:我们使用的:ResourceOwnerPasswordTokenGranter
此时会交给 private final AuthenticationManager authenticationManager 去管理生成:Authentication 此时调用的是:ProviderManager,然后将生成的Authentication作为参数传递给:OAuth2Authentication生成一个OAuth2Authentication返回。
然后通过AbstractTokenGranter生成:OAuth2AccessToken
3.DefaultTokenServices详解
DefaultTokenServices如何把OAuth2AcessToken生成出来的?我们进入DefaultTokenServices中,在如下地方打断点。我们去掉其他断点,然后重新发送请求。
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
//1.tokenStore去查找是否之前的access_token过期
OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null) {
//Access_Token没有过期,因为有可能之前是授权码模式,现在是用户名/密码模式;所以重新存储覆盖:access_token并且返回access_token
if (!existingAccessToken.isExpired()) {
this.tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
this.tokenStore.removeRefreshToken(refreshToken);
}
this.tokenStore.removeAccessToken(existingAccessToken);
}
//2.没有找到之前存储的access_token,所以我们查找:refreshToken没有的话就生成:refreshToken
if (refreshToken == null) {
refreshToken = this.createRefreshToken(authentication);
} else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = this.createRefreshToken(authentication);
}
}
//3.我们根据RefreshToken和OAuth2Authentication创建一个新的OAuth2AccessToken和refreshToken存储起来。
OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
this.tokenStore.storeAccessToken(accessToken, authentication);
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
this.tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
创建信息OAuth2AccessToken的时候会调用增强来个性化自己的OAuth2AccessToken
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = this.getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (long)validitySeconds * 1000L));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
//1.使用accessTokenEnhancer增强生成OAuth2AccessToken
return (OAuth2AccessToken)(this.accessTokenEnhancer != null ? this.accessTokenEnhancer.enhance(token, authentication) : token);
}
33.SpringSecurity-SpringSecurity Oauth授权源码解读
前言
- 我们前几节讲解Spring Security的时候,核心原理就是上图所示的过滤器链路。
- 我们核心考察的是类:FilterSecurityInterceptor;FilterSecurityInterceptor用于最后校验我们的请求是否最后能够到达我们的REST API.如果不能的话,会抛出异常,抛出异常的话,会由于其前面的ExceptionTranslationFilter处理。
- 我们这里与权限相关的主要类是:FilterSecurityInterceptor和ExceptionTranslationFilter
-
我们之前讲解的各种过滤器,现在我们主要讲解的是:AnonymousAuthenticationFilter(匿名认证过滤器),他处于我们上面绿色过滤器最后,不管前面有各种过滤器,后面都会到AnonymousAuthenticationFilter(匿名认证过滤器):看代码之后,主要看其里面的逻辑是判断当前SecurityContextHolder里面是否有:Authentication;也就是说:前面过滤器是否成功进行了成功地身份认证。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { if (SecurityContextHolder.getContext().getAuthentication() == null) { SecurityContextHolder.getContext().setAuthentication(this.createAuthentication((HttpServletRequest)req)); if (this.logger.isDebugEnabled()) { this.logger.debug("Populated SecurityContextHolder with anonymous token: ''" + SecurityContextHolder.getContext().getAuthentication() + "''"); } } else if (this.logger.isDebugEnabled()) { this.logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: ''" + SecurityContextHolder.getContext().getAuthentication() + "''"); } chain.doFilter(req, res); }
-
我们之前说过,认证成功之后,我们返回的是一个:Authentication.我们查看:AnonymousAuthenticationFilter的创建:Authentication 我们知道:里面的用户信息:principal是:anonymousUser,如果前面的过滤器没有认证成功,那么此时 SecurityContextHolder.getContext()的认证信息:Authentication就是AnonymousAuthenticationFilter自己创建的。
protected Authentication createAuthentication(HttpServletRequest request) { AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(this.key, this.principal, this.authorities); auth.setDetails(this.authenticationDetailsSource.buildDetails(request)); return auth; } public AnonymousAuthenticationFilter(String key) { this(key, "anonymousUser", AuthorityUtils.createAuthorityList(new String[]{"ROLE_ANONYMOUS"})); } public AnonymousAuthenticationFilter(String key, Object principal, List<GrantedAuthority> authorities) { this.authenticationDetailsSource = new WebAuthenticationDetailsSource(); Assert.hasLength(key, "key cannot be null or empty"); Assert.notNull(principal, "Anonymous authentication principal must be set"); Assert.notNull(authorities, "Anonymous authorities must be set"); this.key = key; this.principal = principal; this.authorities = authorities; }
- 所以通过上面分析我们知道:如果我们认证成功,那么返回的Authentication里面的principal就是我们UserDetailsService里面封装的UserDetails信息,如果没有认证成功,那么Authentication里面的principa就是上面的"anonymousUser"字符串。
- 不管怎样,最后传给我们FilterSecurityInterceptor的Authentication都会存在,然后FilterSecurityInterceptor决定当前包含的Authentication包含的权限是否可以访问你当前请求的url。
内容
1.SpringSecurity授权逻辑图分析
我们现在主要关注:FilterSecurityInterceptor和ExceptionTranslationFilter,首先我们观察下spring security中和授权相关的类接口以及调用关系。
其中核心的类和接口就只有3个:
FilterSecurityInterceptor、AccessDecisionManager、AccessDecisionVoter。
- FilterSecurityInterceptor是我们Spring Security过滤器链路上最后的拦截器,是我们授权的主入口,FilterSecurityInterceptor其实是一个Filter
- AccessDecisionManager(访问决定管理器),其实是一个接口,他有一个抽象的实现:AbstractAccessDecisionManager和3个具体类;他是一个管理者,管理的是什么了?从名字我们可以看到,他管理的是一组:AccessDecisionVoter(授权决定投票者)
- AccessDecisionVoter会综合所有投票者的投票结果,然后给出一个最终结果过还是不过,具体判断过与不过,有3套这样的逻辑,具体的投票逻辑是在AbstractAccessDecisionManager的子类里面:
a.AffirmativeBased:只有有一个voter投通过,那么整体请求就通过。 b.ConsensusBased:比较投通过和不通过的票数, 哪一种意见多就用哪一种。
c.UnanimousBased:不管有多少个voter投通过,只要有一个投不通过。整个请求不通过。默认spring security默认使用第一种:AffirmativeBased - 那么SecurityConfig和SecurityContextHolder是干什么的呢?我们之前说要判断一个请求是否能够正常访问?需要两方面的数据:a.系统配置信息(具体url需要什么样的信息),这些配置信息我们是配置到:
FilterSecurityInterceptor会从我们的安全配置:SecurityConfig里面信息读出来,封装成一组SecurityAttribute这样的一组对象,这组对象里面其实每一个SecurityAttribute对应着一个url所对应的权限。这里面其实就是你系统配置的信息。
Authentication信息封装的基本认证信息。 - Authentication、ConfigAttribute和FilterSecurityInterceptor携带的请求信息一起传给我们的AccessDecisionManager,然后AccessDecisionManager交给 投票者,由投票者来投票是过还是不过。
2.SpringSecurity源码追踪
我们看两个流程:一个是:用户没登录时候,被拒绝访问流程,另一个是用户登录以后,访问通过流程。
2.1 用户没登录时候,被拒绝访问流程
我们访问:http://127.0.0.1:8088/
断点:FilterSecurityInterceptor
//封装请求、响应、过滤器链到FilterInvocation
FilterInvocation fi = new FilterInvocation(request, response, chain);
//判断:此请求是否经过FilterSecurityInterceptor处理,经过的话直接返回到下一个Filter中。
if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
//处理授权的核心逻辑,就是我们FilterSecurityInterceptor判断是否可以访问RESTAPI,授权主要也在这里,如果授权不通过,则会抛出异常。
InterceptorStatusToken token = super.beforeInvocation(fi);
protected InterceptorStatusToken beforeInvocation(Object object)方法中
//读取我们的SecurityConfig将其封装成一个对象。
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
DefaultFilterInvocationSecurityMetadataSource:
我们可以看到每一个url,所具备的权限。
这个map就是根据我们WebSecurityConfig里面protected void configure(HttpSecurity http) throws Exception()生成的。
//判断SecurityContextHolder.getContext()里面是否有授权信息,因为授权信息我们就算没有认证成功,也会在AnonymousAuthenticationFilter生成,如果确实为空,则有异常了。
if (SecurityContextHolder.getContext().getAuthentication() == null) {
this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes);
}
通过decide方法决定是不是让其通过.
未有授权通过就会抛出异常:异常先从:最先调用的AffirmativeBased抛出异常,然后抛出异常到父类,再次到接口,最后抛出异常到: FilterSecurityInterceptor;然后往前抛到FilterSecurityInterceptor前面的异常转换过滤器(ExceptionTranslationFilter);
然后进入: this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
把我发出去去认证。
把我发出去去认证,其实就是跳到了我们之前在:WebSecurityConfig配置里面loginPage所配置的。
2.1 用户没登录时候,被拒绝访问流程
之前是用户未登录时候的授权流程,我们现在开始登陆。
登录完成之后,我们再次访问:
http://127.0.0.1:8088/user/1
这个hasRole(''ROLE_ADMIN'')是怎么来的呢?
我们在配置文件总配置了:ADMIN
然后进入源码跟踪:ExpressionUrlAuthorizationConfigurer的:
public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry hasRole(String role) {
return this.access(ExpressionUrlAuthorizationConfigurer.hasRole(role));
}
然后进入:ExpressionUrlAuthorizationConfigurer
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException("role should not start with ''ROLE_'' since it is automatically inserted. Got ''" + role + "''");
} else {
return "hasRole(''ROLE_" + role + "'')";
}
}
说明返回的字符串会拼接前缀返回:"hasRole(''ROLE_" + role + "'')"。
所以我们授权时候也需要给:ROLE_ADMIN
上面我们拿到了系统配置和Authentication信息,现在我们进入投票Voter,结果如下:
最后拿到了我们的接口返回信息:
今天的关于配置SpringSecurity前台后台登录处理和springsecurity 配置的分享已经结束,谢谢您的关注,如果想了解更多关于16.SpringSecurity-短信登录配置及重构、23.SpringSecurity-SpringSecurityOAuth简介、25.SpringSecurity-SpringSecurityOAuth核心源码解析、33.SpringSecurity-SpringSecurity Oauth授权源码解读的相关知识,请在本站进行查询。
本文标签: