GVKun编程网logo

配置SpringSecurity前台后台登录处理(springsecurity 配置)

13

这篇文章主要围绕配置SpringSecurity前台后台登录处理和springsecurity配置展开,旨在为您提供一份详细的参考资料。我们将全面介绍配置SpringSecurity前台后台登录处理的

这篇文章主要围绕配置SpringSecurity前台后台登录处理springsecurity 配置展开,旨在为您提供一份详细的参考资料。我们将全面介绍配置SpringSecurity前台后台登录处理的优缺点,解答springsecurity 配置的相关问题,同时也会为您带来16.SpringSecurity-短信登录配置及重构、23.SpringSecurity-SpringSecurityOAuth简介、25.SpringSecurity-SpringSecurityOAuth核心源码解析、33.SpringSecurity-SpringSecurity Oauth授权源码解读的实用方法。

本文目录一览:

配置SpringSecurity前台后台登录处理(springsecurity 配置)

配置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-短信登录配置及重构

16.SpringSecurity-短信登录配置及重构

1.短信登录配置及重构

  • 短信登录一部分配置主要是配置以下3个组件:

image.png
主要是配置: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前面
image.png

1.3 SmsCodeAuthenticationSecurityConfig配置到WebSecurityConfig

我们把spring-security-core里面的SmsCodeAuthenticationSecurityConfig也配置导入到spring-security-web下面的:WebSecurityConfig配置中:
image.png

1.4 测试查看效果

http://127.0.0.1:8088/login.html

image.png

  1. 点击短信登录的登录后返回:

image.png

  1. 点击发送验证码
向手机:13012345678 发送短信验证码:711828  
  1. 我们输入短信验证码,然后登录

image.png
得出结果:
image.png

2.短信登录重构

现在为止,两种登录方式都已经开发完了。

  1. 他们都可以在一个系统里面同时生效。
  2. 底层都是Spring Security,采用的是同一种机制和流程,但是认证的逻辑是不同的。

目前代码重构主要是做两方面内容:
消除重复: 短信验证码和图形验证码里面重复的代码,因为我们是做的拷贝粘铁,然后做了一些简单修改。实现的短信验证码校验功能。 重复的代码是代码坏味道 里面最常见和最严重的。他使得你的代码难以维护,虽然开发时候挺快的,一旦业务逻辑变化,修改代码时候,你可能需要把复制粘贴地方都要修改。重复一个功能模块、小到配置项:比如:在WebSecurityConfig里面:
image.png

WebSecurityController里面:
image.png

都有相同的代码:/authentication/require
以上修改的时候,你同样需要改变一下,改了一个地方,另一个地方忘记改了,也会出问题。

  1. 配置抽象:就像我们把短信验证码授权安全配置一样:SmsCodeAuthenticationSecurityConfig 单独抽象到一个类里面。所以这里我们把验证码统一都抽象出来,短信验证码、图形验证码。短信配置的话 也放到一个配置里面去。

image.png

  1. 不管在app端,还是浏览器端应用的一些配置我们都抽取到spring-security-core里面
  2. 上面基本配置都封装成各个类的话,这样的话在WebSecurityConfig里面只剩下其特有的配置代码;比如:remeberMe就是其特有代码。remeberMe实现是基于往浏览器里面写cookie实现。
  3. App里面我们会单独写一个AppSecurityConfig配置代码
  4. 这样的话AppSecurityConfig、WebSecurityConfig可以使用如下引用方法去引用:

image.png

AppSecurityConfig、WebSecurityConfig需要支持认证登录的话都得配置。

2.1 验证码校验过滤器合并成一个

验证码校验过滤器合并成一个ValidateCodeFilter;其启动时候会根据系统的配置:
image.png

读取到一个map里面去:

/**
 * 存放所有需要校验验证码的url
 */
private Map<String, ValidateCodeType> urlMap = new HashMap<>();

里面存放着什么样的验证码使用什么样的处理器。
根据依赖查找查找到对应的校验码处理器:

image.png

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 所有模块配置抽象出来

image.png

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";
}

image.png

23.SpringSecurity-SpringSecurityOAuth简介

23.SpringSecurity-SpringSecurityOAuth简介

前言

Spring Security OAuth开发App认证框架

基于session实现的保存用户登录信息:

  1. 我们现在用户登录成功之后,用户信息都是存在后端Session的,用户通过浏览器每次去访问的时候,每一次服务器都会去检查浏览器的cookie里面是否存在JSESSIONID,不存在jsessionid,我们就会去服务器创建一个session,然后把新创建的sessionid传回到浏览器cookie中。这样每次用户通过浏览器发请求的时候,我们会根据JSESSIONID找到session,然后找到对应的用户信息。
    image.png

前后端分离架构:

  1. 移动互联网到来后,新的访问渠道出现了:APP,除了这种访问渠道外,应用的部署方式也在不断演进。现在比较流行的方式就是前后端分离。 前后端分离时候,前端资源不跟后端资源在同一个服务器。而是单独部署到一个Web Server上(比如:node.js)。 采用这种前后端分离架构的时候,用户访问的是WebServer。页面渲染和ajax处理都是这个Web Server来处理的。 用户请求发送到Web Server时候,Web Server再将这个请求转发到Application Server上面。
    image.png

新架构变化带来的问题:

  1. 用户不是直接通过浏览器访问我们的Application Server应用,而是通过App第三方应用,或者Web Server;访问我们的内部应用,不是浏览器了,而是其他第三方应用。使用我们的cookie和session就会出现问题。
  2. 这种架构下:能不能用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区别

  1. Spring Social是客户端封装,Spring Security Oauth是服务端封装
  2. Spring Social开发第三方登录时候,实际上封装了第三方应用也就是client他所要做的大部分事情。拿着Spring Social我们可以很快开发一个第三方应用角色去连接我们服务提供商。

image.png

Spring Security Oauth则是封装了服务提供商需要完成的绝大部分的行为;使用Spring Security OAuth,我们就能很快搭建一个服务提供商的程序来。然后往外发送令牌、验收令牌。

image.png

1.2 SpringSecurityOauth简介

image.png

上面绿色的快都是SpringSecurity给我们实现好了的,红色的快是我们需要自己实现的 ;我们实现的这块:资源是标准的服务是不用改的。我们要改的是自定义的认证方式

  1. 要实现Spring Security Oauth服务提供商的内容,其实就是需要实现两个服务器;也就是我们之前介绍的认证服务器(Authorization Server)和 资源服务器(Resource Server)
  2. 在认证服务器里面要做的就是4种授权模式;通过这4种模式来区分用户的身份,以及他所拥有的权限。Spring Security Oauth其实已经把这种模式帮我们实现了。这4种模式实现后,通过用户名/密码.我们知道用户信息之后,我们第二步要做的就是根据我们的这些信息生成Token令牌 即Token的生成存储。后面会根据这个令牌拿去用户信息做检验,所以Token不但生成也得存储。
  3. 资源服务器就是要保障我们的资源:资源其实就是我们在应用里面写的这些rest服务,怎样来保护这些服务呢?按照目前模式我们使用的是Spring Security过滤器链。他是在我们资源之前加一个过滤器链?那么Spring Security OAuth如何实现资源服务器这样一个功能的呢?他就是在SpringSecurity过滤器链上面添加一个新的过滤器:OAuth2AuthenticationProcessingFilter(从请求中拿到你发送出去的token,然后根据你配置的存储策略去存储里面找到用户对应信息,根据这个用户信息是否存在?是否有权限等一系列判断来决定他是否能访问到你决定的资源,从而实现了资源服务器的功能)
  4. 我们其实不希望让用户走这4种标准的授权模式的。比如手机号和短信验证码的登录方式。其实跟我们标准的这4中授权模式是搭不上的。标准的这4中授权模式是没有让你输入手机号、输入短信的。然后我们就发一个token给你。我们需要做自定义的认证方式,让这种自定义的认证方式也可以嫁接到认证服务器上去。用户通过这种自定义的认证方式:用户名/密码、手机号、短信、还有第三方的认证之后调用token机制生成token发送到用户。

1.3 接下来需要做的是

  1. 实现一个标准的OAuth2协议中Provider角色的主要功能:自定义认证
  2. 重构之前的3种认证方式代码,使其支持token
  3. 高级特性(JWT,单点登录)

25.SpringSecurity-SpringSecurityOAuth核心源码解析

25.SpringSecurity-SpringSecurityOAuth核心源码解析

前言

image.png

  1. 之前我们已经完成了上面的服务提供商中:认证服务器、资源服务器的开发。 实际上代码非常简单,就是使用Spring Security OAuth的注解。 有了这些注解后我们已经可以按照标准的OAuth协议往外边发访问令牌access_token。上节演示了如何用授权码模式,密码模式获取access_token。然后拿着这些令牌去访问我们的资源服务器中的资源。
  2. 以上标准流程做完之后,我们接下来需要做的就是把我们之前做的三种模式(短信登录、QQ登录、微信登录)嫁接到标准模式中去。让我们在这三种模式认证通过之后。也可以返回access_token。那么为了实现这个目的,我们需要实现上面这个源码。

内容

1.Spring Security Oauth核心源码

image.png

  1. 上图中:绿色方块表示实体类,蓝色方块表示接口,括号中为真正实现类。
  2. TokenEndpoint是程序入口点,可以理解成一个controller,他会处理令牌请求。上一节中我们用授权码模式、密码模式时候,请求的url都是一致的,他是通过grant\_type来告知我们使用的是哪种请求模式。他处理/oauth/token请求。当收到令牌请求时候,TokenEndPoint会调用我们的ClientDetailsService
  3. ClientDetailsService区别于UserDetailsService;UserDetailsService是读取用户信息的,ClientDetailsService用来读取第三方应用信息的。我们之前发送请求时候在Header里面都会携带clientId和clientSecret来告诉是哪个应用请求授权,这个ClientDetailsService就会根据传过来的clientId和clientSecret读取响应的client配置信息。这些配置信息都会读取到ClientDetails这个对象里面去。
  4. ClientDetails:这里面封装的是第三方应用的信息,然后TokenEndpoint还会创建一个TokenRequest对象。
  5. TokenRequest:封装了我们请求的其他参数信息,比如:grant_type;如果是密码模式的话,我们的用户名、密码是什么?同时也会把ClientDetails信息放到TokenRequest里面去。因为第三方应用信息 也是令牌请求一部分。利用TokenRequest会去调用我们的TokenGranter(令牌授权者)接口,这个接口后面其实封装了我们的4种授权模式的不同实现。这个接口里面会根据你接口传上来的grant_type去挑选一个自己的实现去执行令牌的生成。不管是哪种实现,生成过程中都会产生2种东西:OAuth2Request和Authentication。
  6. OAuth2Request其实就是之前ClientDetails和TokenRequest信息的整合。
  7. Authentication接口封装了当前授权用户的一些信息。谁在做一些授权,授权用户信息就是在Authentication接口里面。它里面的信息是我们通过UserDetailsService读出来的。
  8. OAuth2Authentication:OAuth2Request和Authentication整合形成。它里面包含了现在是哪一个第三方应用在请求哪个用户在授权,然后用的授权模式是什么?授权参数是什么等?然后会传递给接口:AuthorizationServerTokenServices
  9. AuthorizationServerTokenServices拿到我们OAuth2Authentication之后,他会最终生成一个OAuth2AccessToken令牌。AuthorizationServerTokenServices的默认实现的:DefaultTokenServices里面含有两个接口的引用:TokenStore和TokenEnhancer
  10. TokenStore用来处理令牌存储。
  11. TokenEnhancer令牌生成器,当我们令牌生成之后,我们可以自定义去改造令牌。

2.源码追踪

我们使用密码模式追踪。因为授权码模式需要两步(第一步获取授权码,然后拿着授权码去获取token),测试起来比较麻烦。

image.png

然后生成TokenRequest:
image.png

//根据请求参数和客户端信息创建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里面:

image.png

里面会存在包含refresh\_token的5种模式,一次遍历找到对应目前人的授权。

然后根据传入的grantType参数,找到对应的授权类型,生成: ,然后返回回去,产生最终令牌:

密码模式的实现:
image.png

然后进入:我们使用的:ResourceOwnerPasswordTokenGranter

image.png
image.png

此时会交给 private final AuthenticationManager authenticationManager 去管理生成:Authentication 此时调用的是:ProviderManager,然后将生成的Authentication作为参数传递给:OAuth2Authentication生成一个OAuth2Authentication返回。

image.png

然后通过AbstractTokenGranter生成:OAuth2AccessToken
image.png

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授权源码解读

33.SpringSecurity-SpringSecurity Oauth授权源码解读

前言

image.png

  1. 我们前几节讲解Spring Security的时候,核心原理就是上图所示的过滤器链路。
  2. 我们核心考察的是类:FilterSecurityInterceptor;FilterSecurityInterceptor用于最后校验我们的请求是否最后能够到达我们的REST API.如果不能的话,会抛出异常,抛出异常的话,会由于其前面的ExceptionTranslationFilter处理。
  3. 我们这里与权限相关的主要类是:FilterSecurityInterceptor和ExceptionTranslationFilter
  4. 我们之前讲解的各种过滤器,现在我们主要讲解的是: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);
         }
  5. 我们之前说过,认证成功之后,我们返回的是一个: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;
     }
  6. 所以通过上面分析我们知道:如果我们认证成功,那么返回的Authentication里面的principal就是我们UserDetailsService里面封装的UserDetails信息,如果没有认证成功,那么Authentication里面的principa就是上面的"anonymousUser"字符串。
  7. 不管怎样,最后传给我们FilterSecurityInterceptor的Authentication都会存在,然后FilterSecurityInterceptor决定当前包含的Authentication包含的权限是否可以访问你当前请求的url。

内容

1.SpringSecurity授权逻辑图分析

image.png

我们现在主要关注:FilterSecurityInterceptor和ExceptionTranslationFilter,首先我们观察下spring security中和授权相关的类接口以及调用关系。

其中核心的类和接口就只有3个:
FilterSecurityInterceptor、AccessDecisionManager、AccessDecisionVoter。

  1. FilterSecurityInterceptor是我们Spring Security过滤器链路上最后的拦截器,是我们授权的主入口,FilterSecurityInterceptor其实是一个Filter
  2. AccessDecisionManager(访问决定管理器),其实是一个接口,他有一个抽象的实现:AbstractAccessDecisionManager和3个具体类;他是一个管理者,管理的是什么了?从名字我们可以看到,他管理的是一组:AccessDecisionVoter(授权决定投票者)
  3. AccessDecisionVoter会综合所有投票者的投票结果,然后给出一个最终结果过还是不过,具体判断过与不过,有3套这样的逻辑,具体的投票逻辑是在AbstractAccessDecisionManager的子类里面:
    a.AffirmativeBased:只有有一个voter投通过,那么整体请求就通过。 b.ConsensusBased:比较投通过和不通过的票数, 哪一种意见多就用哪一种。
    c.UnanimousBased:不管有多少个voter投通过,只要有一个投不通过。整个请求不通过。默认spring security默认使用第一种:AffirmativeBased
  4. 那么SecurityConfig和SecurityContextHolder是干什么的呢?我们之前说要判断一个请求是否能够正常访问?需要两方面的数据:a.系统配置信息(具体url需要什么样的信息),这些配置信息我们是配置到:
    image.png
    FilterSecurityInterceptor会从我们的安全配置:SecurityConfig里面信息读出来,封装成一组SecurityAttribute这样的一组对象,这组对象里面其实每一个SecurityAttribute对应着一个url所对应的权限。这里面其实就是你系统配置的信息。
    Authentication信息封装的基本认证信息。
  5. Authentication、ConfigAttribute和FilterSecurityInterceptor携带的请求信息一起传给我们的AccessDecisionManager,然后AccessDecisionManager交给 投票者,由投票者来投票是过还是不过。

2.SpringSecurity源码追踪

我们看两个流程:一个是:用户没登录时候,被拒绝访问流程,另一个是用户登录以后,访问通过流程。

2.1 用户没登录时候,被拒绝访问流程

我们访问:http://127.0.0.1:8088/
断点:FilterSecurityInterceptor
image.png

//封装请求、响应、过滤器链到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:

image.png
我们可以看到每一个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);
                }

image.png

image.png

通过decide方法决定是不是让其通过.
image.png

image.png

未有授权通过就会抛出异常:异常先从:最先调用的AffirmativeBased抛出异常,然后抛出异常到父类,再次到接口,最后抛出异常到: FilterSecurityInterceptor;然后往前抛到FilterSecurityInterceptor前面的异常转换过滤器(ExceptionTranslationFilter);

image.png

然后进入: this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);

image.png

把我发出去去认证。
image.png
image.png
把我发出去去认证,其实就是跳到了我们之前在:WebSecurityConfig配置里面loginPage所配置的。
image.png
image.png

image.png

2.1 用户没登录时候,被拒绝访问流程

之前是用户未登录时候的授权流程,我们现在开始登陆。
image.png

登录完成之后,我们再次访问:

http://127.0.0.1:8088/user/1

image.png

这个hasRole(''ROLE_ADMIN'')是怎么来的呢?
我们在配置文件总配置了:ADMIN
image.png

然后进入源码跟踪: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

image.png

上面我们拿到了系统配置和Authentication信息,现在我们进入投票Voter,结果如下:

image.png
image.png

最后拿到了我们的接口返回信息:
image.png

今天的关于配置SpringSecurity前台后台登录处理springsecurity 配置的分享已经结束,谢谢您的关注,如果想了解更多关于16.SpringSecurity-短信登录配置及重构、23.SpringSecurity-SpringSecurityOAuth简介、25.SpringSecurity-SpringSecurityOAuth核心源码解析、33.SpringSecurity-SpringSecurity Oauth授权源码解读的相关知识,请在本站进行查询。

本文标签: