GVKun编程网logo

Shiro与SpringSecurity的比较(springs和spring的区别)

7

如果您想了解Shiro与SpringSecurity的比较的相关知识,那么本文是一篇不可错过的文章,我们将对springs和spring的区别进行全面详尽的解释,并且为您提供关于23.SpringSe

如果您想了解Shiro与SpringSecurity的比较的相关知识,那么本文是一篇不可错过的文章,我们将对springs和spring的区别进行全面详尽的解释,并且为您提供关于23.SpringSecurity-SpringSecurityOAuth简介、25.SpringSecurity-SpringSecurityOAuth核心源码解析、33.SpringSecurity-SpringSecurity Oauth授权源码解读、34.SpringSecurity-SpringSecurity Oauth权限表达式的有价值的信息。

本文目录一览:

Shiro与SpringSecurity的比较(springs和spring的区别)

Shiro与SpringSecurity的比较(springs和spring的区别)

我目前正在评估基于Java的安全框架,我是Spring 3.0用户,因此似乎似乎SpringSecurity是正确的选择,但是Spring安全性似乎受到过分复杂的困扰,它似乎并没有使安全性易于实现, Shiro似乎更加连贯,更容易理解。我正在寻找这两个框架之间的利弊清单。

答案1

小编典典

我也同意Spring Security对我来说感觉太复杂了。当然,他们已经做了一些降低复杂性的事情,例如创建自定义XML名称空间以减少XML配置的数量,但是对我来说,这些并不能解决我个人关于Spring Security的基本问题:其名称和概念通常会使人困惑,我。很难“得到它”。

不过,你开始使用Shiro的第二个步骤就是“得到它”。在安全世界中很难理解的东西就更容易理解了。在JDK中难以使用的东西(例如Ciphers)被简化到不仅可以忍受的程度,而且常常使人乐于使用。

例如,如何在Java或Spring Security中对密码进行哈希+盐加上base64编码?都不如Shiro的解决方案那么简单直观:

ByteSource salt = new SecureRandomNumberGenerator().nextBytes();new Sha512Hash(password, salt).toBase64();

不需要通用编解码器或其他任何东西。只是四郎jar。

现在关于Spring环境,大多数Shiro开发人员都将Spring作为其主要应用程序环境。这意味着Shiro的Spring集成非常出色,并且一切都非常好。你可以放心,如果你正在编写Spring应用程序,那么你将拥有全面的安全体验。

例如,在该线程的另一篇文章中考虑Spring XML配置示例。这是你在Shiro(基本上)将做的事情(基本上):

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd><bean id="shiroFilter">    <property name="securityManager" ref="securityManager"/>    <property name="loginUrl" value="/login.jsp"/>    <property name="successUrl" value="/home.jsp"/>    <property name="unauthorizedUrl" value="/unauthorized.jsp"/>    <property name="filterChainDefinitions">        <value>        /secure/** = authc        /** = anon        </value>    </property></bean><bean id="securityManager">    <property name="realm" ref="myRealm"/></bean><bean id="myRealm">    ...</bean>

尽管比其他Spring示例更加详细,但更易于阅读IMO。

你还会发现使用Shiro的过滤器链定义可能是定义常规过滤器链和基于Web的安全规则的最简单方法!比在web.xml中定义更好。

最后,Shiro还提供了极高的“可插拔性”。你会发现由于Shiro的POJO /注入友好架构,你几乎可以配置和/或替换任何东西。Shiro几乎将所有默认设置都设为默认设置,你可以覆盖或仅配置所需的设置。

归根结底,我认为选择这两个选项中的任何一个都更适合你的心理模型-两者中哪个更有意义并且对你来说更直观?对于某些人来说将是Shiro,对于其他人来说将是Spring Security。Shiro在Spring环境中运行良好,因此我想根据两者中的哪一个选择你更喜欢的并且对你来说最有意义。

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

34.SpringSecurity-SpringSecurity Oauth权限表达式

34.SpringSecurity-SpringSecurity Oauth权限表达式

前言

image.png

  1. 之前我们看了Spring Security在控制授权这一块他的核心代码,我们看到最后请求通过或者不通过都是转成了权限的表达式,然后交给了一个WebExpressionVoter去评估你的表达式。这个表达式评估的结果是true,那么访问就通过,如果评估的结果是false,那么访问就不通过。那么问题来了,SpringSecurity有多少种权限表达式?每一种权限表达式的写法是怎样的?代表的意思是什么?

内容

1. 表达式说明

image.png
上面的表达式都是在WebSecurityConfig可以配置的: 每一个表达式都是对应了HttpSecurity的一个方法,并且是跟在antMatchers之后的。
image.png
比如:antMatchers("xxx").permitAll();
antMatchers---指定url
表达式---指定授权

如果需要多个表达式合并统一起来,需要自己通过.access来自己定义: image.png
另一个需求是我们能不能通过.access自定义逻辑:让系统读取我们自己的逻辑而不是使用系统的默认逻辑。

2. 剥离用户自己模块的url服务

2.1 抽离思想

目前我们的安全配置是写在我们的安全模块代码spring-security-core里面的,不管是spring-security-web和spring-security-app.都有一些针对url的安全配置。但是问题是这些url有些是我们的安全模块提供的 ,比如说一下url:
image.png
都是安全模块提供的。
但是用户注册("/user/register"),和获取用户的url("/user/*"),他其实是我们的spring-security-demo项目提供的。也就是使用我们安全模块的人提供的安全服务。对于安全模块来说,事先我们并不知道谁会使用我们的安全模块。我也不知道使用此安全模块的url服务。所以针对于使用安全模块的url服务我们应该剥离出去。

image.png

实现思路很简单,我们提供一个接口,在我们自己的权限模块里面去实现这个接口(把和安全模块相关的配置写到这个接口里面)。如果是用户模块的话,用户模块自己去实现此接口(将用户模块的url写到用户模块中) 如果我们应用A都需要多有的url服务,那么我们就让应用A去依赖这两个模块。应用A也有这个AuthorizeConfigProvider的实现。所以在应用A的spring容器中 一共有3个模块的权限配置提供者的实现:他们是权限模块、用户模块、应用A实现模块。

1.权限模块:权限模块自身的url权限配置
2.用户模块:用户模块的url权限配置
3.应用A实现模块:应用A特有的url权限配置

最后,在我们的权限模块core中只需要提供一个:AuthorizeConfigManager类,这个类的作用是:把Spring容器里面所有AuthorizeConfigProvider接口的实现全部收集起来。然后按照各个实现的配置给配置好。

假如我们现在有一个应用B,应用B也有自己的AuthorizeConfigProvider的实现并且依赖了权限模块实现、用户模块实现,此时AuthorizeConfigManager类也会收集起三个模块:权限模块、用户模块、应用A实现模块。最终应用A和应用B权限是不同的,但是我们的权限模块core是不管的。

2.2 抽离-代码实现

2.2.1 spring-security-core创建AuthorizeConfigProvider
  1. 我们创建包:com.yxm.security.core.authorize,然后在此包下创建接口:AuthorizeConfigProvider
  2. 我们从spring-security-web的WebSecurityConfig的配置里面获取授权url开始的权限对象:
    image.png
  3. authorizeRequests()返回的对象是:
    image.png
  4. 我们将其封装到授权配置provider中去:

    public interface AuthorizeConfigProvider {
     void  configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config);
    }
2.2.2 core实现AuthorizeConfigProvider

MyAuthorizeConfigProvider实现AuthorizeConfigProvider 接口,并申明为Spring的组件。

@Component
public class MyAuthorizeConfigProvider implements AuthorizeConfigProvider {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
        config.antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
                   SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
                   securityProperties.getBrowser().getLoginPage(),
                   SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/*",
                   securityProperties.getBrowser().getSignUpUrl())
                .permitAll();
    }
}
2.2.3 core中定义AuthorizeConfigManager
public interface AuthorizeConfigManager {
    void  configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config);
}
2.2.4 core中定义AuthorizeConfigManager的实现:MyAuthorizeConfigManager
@Component
public class MyAuthorizeConfigManager implements AuthorizeConfigManager {
    /**
     * 作用:把系统的provider全部收集起来
     * @param config
     */
    @Autowired
    private Set<AuthorizeConfigProvider> authorizeConfigProviders;

    @Override
    public void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
         for (AuthorizeConfigProvider authorizeConfigProvider:authorizeConfigProviders){
             authorizeConfigProvider.configure(config);
         }

         //除了上面配置的所有权限外,其他请求都需要授权
        config.anyRequest().authenticated();
    }
}
2.2.5 web里面改造
@Configuration
public class WebSecurityConfig extends AbstractChannelSecurityConfig {

    @Autowired
    private ValidateCodeSecurityConfig validateCodeSecurityConfig;//验证码过滤器配置

    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; //短信验证码授权配置

    @Autowired
    private SpringSocialConfigurer mySocialSecurityConfig;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private AuthorizeConfigManager authorizeConfigManager;

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        //
        //因为是Jdbc操作,所以我们需要注入数据源:org.springframework.jdbc.core.support.JdbcDaoSupport
        //tokenRepository继承org.springframework.jdbc.core.support.JdbcDaoSupport
        tokenRepository.setDataSource(dataSource);
        System.out.println("PersistentTokenRepository--dataSource:>dataSource");
        //tokenRepository.setCreateTableOnStartup(true);//系统启动的时候创建:CREATE_TABLE_SQL表
        return tokenRepository;

    }
    /**
     * 定义web安全配置类:覆盖config方法
     * 1.参数为HttpSecurity
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        applyPasswordAuthenticationConfig(http);

         http.apply(validateCodeSecurityConfig)
                .and()
             .apply(smsCodeAuthenticationSecurityConfig)
                .and()
             .apply(mySocialSecurityConfig)//配置第三方social
                 .and()
             .rememberMe()
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())//配置token失效秒数
                .userDetailsService(userDetailsService)
                .and()
                 .csrf().disable();

        authorizeConfigManager.configure(http.authorizeRequests());
   }
}
2.2.6 spring-security-app里面改造
@Configuration
@EnableResourceServer
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; //短信验证码授权配置
    @Autowired
    private SpringSocialConfigurer mySocialSecurityConfig;
    @Autowired
    private SecurityProperties securityProperties;
    @Autowired
    private ValidateCodeSecurityConfig validateCodeSecurityConfig;//验证码过滤器配置

    @Autowired
    private OpenIdAuthenticationSecurityConfig openIdAuthenticationSecurityConfig;

    @Autowired
    private AuthorizeConfigManager authorizeConfigManager;


    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
                .loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM)
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler);

        http.apply(validateCodeSecurityConfig)
                .and()
                .apply(smsCodeAuthenticationSecurityConfig)
                .and()
                .apply(mySocialSecurityConfig)//配置第三方social
                .and()
                .apply(openIdAuthenticationSecurityConfig)
                .and()
                .csrf().disable();

        authorizeConfigManager.configure(http.authorizeRequests());
    }
}
2.2.7 spring-security-demo改成

除了core里面的配置,其他用户注册和用户获取的授权都是demo项目自己的配置。

首先先实现:AuthorizeConfigProvider的类:DemoAuthorizeConfigProvider

@Component
public class DemoAuthorizeConfigProvider implements AuthorizeConfigProvider {
    @Override
    public void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
         config.antMatchers("/user").hasRole("ADMIN");
    }
}

然后在授权时候返回的用户配置上即可:

image.png

3.测试

我们访问登录页面http://127.0.0.1:8088/login.html
image.png

访问:http://127.0.0.1:8088/user时候:
image.png

关于Shiro与SpringSecurity的比较springs和spring的区别的问题我们已经讲解完毕,感谢您的阅读,如果还想了解更多关于23.SpringSecurity-SpringSecurityOAuth简介、25.SpringSecurity-SpringSecurityOAuth核心源码解析、33.SpringSecurity-SpringSecurity Oauth授权源码解读、34.SpringSecurity-SpringSecurity Oauth权限表达式等相关内容,可以在本站寻找。

本文标签: