• 使用SpringSecurity搭建授权认证服务(1) -- 基本demo认证原理


    使用SpringSecurity搭建授权认证服务(1) -- 基本demo

    登录认证是做后台开发的最基本的能力,初学就知道一个interceptor或者filter拦截所有请求,然后判断参数是否合理,如此即可。当涉及到某些接口权限的时候,则if-else判断以下,也是没问题的。
    但如果判断多了,业务逻辑也掺杂在一起,降低可读性的同时也不利于扩展和维护。于是就出现了apache shiro, spring security这样的框架,抽离出认证授权判断。
    由于我现在的项目都是给予springboot的,那选择spring security就方便很多。接下来基于此构建我的认证授权服务: 基于Token的认证授权服务。

    项目初始化

    第一个版本,项目初始化https://github.com/Ryan-Miao/spring-security-token-login-server/releases/tag/v1.0

    首先学习两个单词:

    authentication 身份验证

    authorized 经授权的

    我们登录鉴权就是两个步骤,先认证登录,然后权限校验。对应到Spring Security里就是 AuthenticationManagerAccessDecisionManager,前者负责对用户凭证进行认证,后者对认证后的权限进行校验。

    首先,创建一个基本的springboot项目。

    • 引入Springboot, Mybatis, Redis, Swagger, Spring Security
    • 配置全局异常拦截ExceptionInterceptor
    • 配置Redis缓存,这里使用redisson,也可以直接使用starter
    • 配置Spring Security Config

    Spring Security参照官方文档配置即可。接下来是自定义和可以修改的地方。

    数据表权限模型

    本项目简单使用 user - role -permission的模型。

    • 一个user可以有多个role
    • 一个role可以指定给多个user
    • 一个role可以拥有多个permission
    • 一个permission也可以从属于多个role

    权限判定通过判断user是否拥有permission来决定。通过role实现了user和permission之间的解耦,创建多个role模型,绑定对应的权限,当添加新用户的时候,直接指定role就可以授权。

    Spring Security自带了org.springframework.security.provisioning.JdbcUserDetailsManager,它里面的模型为user-group-authority. 即用户归属用户组,用户组有权限。差不多可以和当前模型一一对应。

    认证流程

    大体认证流程和涉及的核心类如下:

    ApplicationFilterChain的filter顺序:

    FilterChainProxy(springSecurityFilterChain)执行认证的顺序, 忽略的url将不命中任何filter, 而需要认证的url将通过VirtualFilterChain来认证。

    使用Token认证

    starter默认启用的基于用户名密码的basic认证。

    通过UsernamePasswordAuthenticationFilter组装UsernamePasswordAuthenticationToken去认证。

    通过org.springframework.security.web.authentication.www.BasicAuthenticationFilter解析header Authorization, 然后组装成UsernamePasswordAuthenticationToken去给AuthenticationManager认证。

    我们要做的就是模仿UsernamePasswordAuthenticationFilter或者BasicAuthenticationFilter解析header将我们的认证凭证传递给AuthenticationManager.

    两种方式我实现了一遍,最终选择了基于UsernamePasswordAuthenticationFilter来实现。

    
    /**
     * @author Ryan Miao
     * @date 2019/5/30 10:11
     * @see org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
     */
    public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
        public TokenAuthenticationFilter(String defaultFilterProcessesUrl) {
            super(defaultFilterProcessesUrl);
        }
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
            boolean debug = this.logger.isDebugEnabled();
            String token = TokenUtils.readTokenFromRequest(request);
            if (StringUtils.isBlank(token)) {
                throw new UsernameNotFoundException("token not found");
            }
    
            if (debug) {
                this.logger.debug("Token Authentication Authorization header found ");
            }
    
            //token包装类, 使用principal来装载token
            UsernamePasswordAuthenticationToken tokenAuthenticationToken = new UsernamePasswordAuthenticationToken(
                token, null);
    
            //AuthenticationManager 负责解析
            Authentication authResult = getAuthenticationManager()
                .authenticate(tokenAuthenticationToken);
            if (debug) {
                this.logger.debug("Authentication success: " + authResult);
            }
    
            return authResult;
        }
    
        /**
         * 重写认证成功后的方法,不跳转.
         */
        @Override
        protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug(
                    "Authentication success. Updating SecurityContextHolder to contain: " + authResult);
            }
    
            SecurityContextHolder.getContext().setAuthentication(authResult);
            getRememberMeServices().loginSuccess(request, response, authResult);
            if (this.eventPublisher != null) {
                this.eventPublisher.publishEvent(
                    new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
            }
    
            chain.doFilter(request, response);
        }
    }
    
    1. TokenUtils来从request里拿到我们的凭证,我这里是从cookie里取出token的值。
    2. 封装给UsernamePasswordAuthenticationToken的username字段
    3. 交给getAuthenticationManager()去认证

    认证Provider

    上一步拿到用户凭证,接下来就是对凭证进行认证。由AuthenticationManager提供。简单理解下AuthenticationManager是什么。

    public interface AuthenticationManager {
    	// ~ Methods
    
    	/**
    	 * Attempts to authenticate the passed {@link Authentication} object, returning a
    	 * fully populated <code>Authentication</code> object (including granted authorities)
    	 * if successful.
    	 * <p>
    	 * An <code>AuthenticationManager</code> must honour the following contract concerning
    	 * exceptions:
    	 * <ul>
    	 * <li>A {@link DisabledException} must be thrown if an account is disabled and the
    	 * <code>AuthenticationManager</code> can test for this state.</li>
    	 * <li>A {@link LockedException} must be thrown if an account is locked and the
    	 * <code>AuthenticationManager</code> can test for account locking.</li>
    	 * <li>A {@link BadCredentialsException} must be thrown if incorrect credentials are
    	 * presented. Whilst the above exceptions are optional, an
    	 * <code>AuthenticationManager</code> must <B>always</B> test credentials.</li>
    	 * </ul>
    	 * Exceptions should be tested for and if applicable thrown in the order expressed
    	 * above (i.e. if an account is disabled or locked, the authentication request is
    	 * immediately rejected and the credentials testing process is not performed). This
    	 * prevents credentials being tested against disabled or locked accounts.
    	 *
    	 * @param authentication the authentication request object
    	 *
    	 * @return a fully authenticated object including credentials
    	 *
    	 * @throws AuthenticationException if authentication fails
    	 */
    	Authentication authenticate(Authentication authentication)
    			throws AuthenticationException;
    }
    

    尝试认证传递过来的Authentication对象(即我们的UsernamePasswordAuthenticationToken), 如果认证通过,返回全部信息以及authority权限,否则抛出AuthenticationException异常表示认证失败。

    AuthenticationManager的初始化比较复杂,绕了好多路。在我们的SecurityConfig里可以找到声明的地方。

    //com.example.serverapi.config.SecurityConfig#authenticationManagerBean
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    //com.example.serverapi.config.SecurityConfig#configure(org.springframework.security.config.annotation.web.builders.HttpSecurity)
    TokenAuthenticationFilter filter = new TokenAuthenticationFilter("/**");
            filter.setAuthenticationManager(authenticationManagerBean());
    

    而AuthenticationManager是AuthenticationManagerDelegator来代替的,其代理的则是org.springframework.security.authentication.ProviderManager。

    所以,我们定义provider来认证上一步的token是否合法

    //com.example.serverapi.config.SecurityConfig#configure(org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder)
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //DaoAuthenticationConfigurer-DaoAuthenticationProvider用来提供登录时用户名和密码认证
        //auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        //自定义TokenAuthenticationProvider, 用来提供token认证
        auth.authenticationProvider(new UserTokenAuthenticationProvider());
    
    }
    

    以下是provider全部信息

    //com.example.serverapi.domain.security.config.UserTokenAuthenticationProvider
    /**
     * 这里只使用了username字段。
     *
     * @author Ryan Miao
     * @date 2019/5/29 22:05
     */
    public class UserTokenAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    
    
        @Override
        protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    
        }
    
    
        @Override
        protected UserDetails retrieveUser(String token,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
            //验证token
            TokenManagement tokenManagement = ServerApiApplication.context
                .getBean(TokenManagement.class);
            com.example.serverapi.domain.security.entity.User userInfo = tokenManagement.get(token);
    
            if (userInfo == null) {
                throw new BadCredentialsException("token认证失败");
            }
    
            authentication.setDetails(userInfo);
    
            Set<SimpleGrantedAuthority> authorities = userInfo.getRoleList().stream()
                .map(Role::getPermissionList)
                .flatMap(Collection::stream)
                .map(p -> new SimpleGrantedAuthority(p.getName())).collect(
                    Collectors.toSet());
            return new User(userInfo.getUsername(), userInfo.getPassword(), authorities);
        }
    
        /**
         * 对应我们的Token令牌类UsernamePasswordAuthenticationToken,可以采用本provide验证.
         */
        @Override
        public boolean supports(Class<?> authentication) {
            return (UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authentication));
        }
    }
    
    • supports方法来表示本provider提供的认证范围,即传递UsernamePasswordAuthenticationToken的凭证将接受认证
    • 自定义了我们自己的Token管理方法TokenManagement,来对token进行认证。根据token拿到userinfo则成功
    • 从userInfo里提取authority,创建一个UserDetails,交给下一步的权限校验

    权限校验

    前面截图里的filter chain,最前面是我们的自定义filter来认证的,最后面的FilterSecurityInterceptor则是权限校验。

    //spring-security-core-5.1.4.RELEASE-sources.jar!/org/springframework/security/access/intercept/AbstractSecurityInterceptor.java:229
    Authentication authenticated = authenticateIfRequired();
    
    // Attempt authorization
    try {
        this.accessDecisionManager.decide(authenticated, object, attributes);
    }
    catch (AccessDeniedException accessDeniedException) {
        publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                accessDeniedException));
    
        throw accessDeniedException;
    }
    
    

    可以看到,调用accessDecisionManager来判断是否继续,权限不足则抛出AccessDeniedException,对应处理就是403了。

    AccessDecisionManager目前看到有两种,一个是全局配置,在我们配置Security Config里指定哪些url需要哪些权限。一个是method级别的配置,通过前者校验后判断method是否有权限。

    AbstractAccessDecisionManager提供了3种方式。

    • AffirmativeBased 任意一种权限校验voter方式通过即通过
    • UnanimousBased 必须所有voter通过才可以通过,即任意失败则不通过
    • ConsensusBased 通过的voter大于拒绝的voter则通过
    • 其他,可以自己实现AbstractAccessDecisionManager

    Voter是什么呢?AccessDecisionVoter是真正判断权限的地方。通过对比当前登录用户的authority权限和要访问的资源的权限比较,返回如下code。

    • int ACCESS_GRANTED = 1;
    • int ACCESS_ABSTAIN = 0;
    • int ACCESS_DENIED = -1;

    权限移除前缀ROLE_

    Spring Security默认使用ROLE_作为authority的前缀,然后表达式里的hasRole, hasAuthority几乎等价,这让我一直很困惑。尤其是当我使用user-role-permission模型的时候,差点以为hasRole是角色判断。所以,为了避免混淆,决定把ROLE_的前缀去掉。

    方法就是声明一个类, 具体理由可以追寻源码hasRole来确定。

    @Bean
    GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
    }
    

    统一超级权限admin

    到现在差不多已经可以实现用户权限校验了。我们给需要权限敏感的api添加注解,比如@PreAuthorize("hasRole('can_list_user')"), 然后permission表里添加can_list_user, 然后角色表role绑定permission,最后把role指派给user。

    然而,当系统需要权限的地方特别多的时候,绑定role的代价也很高。比如,我们需要一个超级管理员admin角色,那么这个admin就必须把所有的permission绑定一遍。想想就恐怖。

    既然理解了Spring Security的权限校验方式,那么就可以自定义了。我们指定带有admin的authority直接通过,无需校验其他权限。

    
    /**
     * 允许设计admin权限的用户直接通过所有认证
     * @author Ryan Miao
     * @date 2019/6/12 20:49
     */
    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
    public class GlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    
        @Override
        protected AccessDecisionManager accessDecisionManager() {
            List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<AccessDecisionVoter<? extends Object>>();
            ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
            expressionAdvice.setExpressionHandler(getExpressionHandler());
            decisionVoters
                .add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
            decisionVoters.add(new RoleVoter());
            decisionVoters.add(new AuthenticatedVoter());
            decisionVoters.add(new AdminVoter());
            return new AffirmativeBased(decisionVoters);
        }
    }
    
    
    /**
     * 拥有admin权限的角色,直接包含所有权限
     *
     * @author Ryan Miao
     * @date 2019/6/12 20:00
     */
    public class AdminVoter implements AccessDecisionVoter<Object> {
    
        private static final String ADMIN = "admin";
    
        @Override
        public boolean supports(ConfigAttribute attribute) {
            return true;
        }
    
        @Override
        public int vote(Authentication authentication, Object object,
            Collection<ConfigAttribute> attributes) {
            if (authentication == null) {
                return ACCESS_DENIED;
            }
            int result = ACCESS_ABSTAIN;
            Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
    
            for (ConfigAttribute attribute : attributes) {
                if (this.supports(attribute)) {
                    result = ACCESS_DENIED;
    
                    // Attempt to find a matching granted authority
                    for (GrantedAuthority authority : authorities) {
                        if (ADMIN.equals(authority.getAuthority())) {
                            return ACCESS_GRANTED;
                        }
                    }
                }
            }
    
            return result;
        }
    
        Collection<? extends GrantedAuthority> extractAuthorities(
            Authentication authentication) {
            return authentication.getAuthorities();
        }
    
        @Override
        public boolean supports(Class clazz) {
            return true;
        }
    }
    

    总结

    初步梳理了Spring Security的认证逻辑和流程,细节的地方还很多,比如SpEL的实现逻辑。但差不多可以理解认证授权是如何实现的了,基于此也足够开展我们的业务开发了。如果说还有想要改造的地方,就是动态权限修改了,为了简化逻辑模型,不做动态权限设定,所有权限初始化指定即可。简单最重要!

  • 相关阅读:
    获取 鼠标 键盘 最后响应信息
    EF 6 for mysql
    Python中的OS模块。
    python中的随机函数random的用法示例random
    Python中OSI七层模型
    Pythoon中的面向对象三大特征
    Python中的函数(学习日记)
    Python的6种运算符(日记)
    计算机的发展史
    osi七层
  • 原文地址:https://www.cnblogs.com/woshimrf/p/spring-security-token-server.html
Copyright © 2020-2023  润新知