• 【SpringSecurity】初识与集成


    个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道

    如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充


    前言

    之前介绍过Shiro,作为SpringSecurity的精简版,已经具备了大部分常用功能,且更加便于使用,因而一定程度上成为了SpringSecurity的替代品。

    相比之下,SpringSecurity功能更加强大完善,通过调整和组合其中的组件,能得到一个高度自定义的安全框架。

    本文中,将基于SpringSecurity+Jwt进行学习。


    1.介绍

    1.1.简介

    SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。

    由于它是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在Spring Boot项目中加入SpringSecurity更是十分简单,使用SpringSecurity 减少了为企业系统安全控制编写大量重复代码的工作。


    1.1.1.SpringSecurity主要包括两个目标

    • 认证(Authentication):建立一个他声明的主题,即确认用户可以访问当前系统。
    • 授权(Authorization):确定一个主体是否允许在你的应用程序执行一个动作,即确定用户在当前系统下所有的功能权限。

    Shiro及其他很多安全框架也以此为目标


    1.1.2.SpringSecurity的认证模式

    在身份验证层,Spring Security 的支持多种认证模式。

    这些验证绝大多数都是要么由第三方提供,或由相关的标准组织,如互联网工程任务组开发。

    另外Spring Security 提供自己的一组认证功能,内容过长,此处不再赘述,有兴趣自己了解。


    1.1.3.核心组件

    • SecurityContextHolder:提供对SecurityContext的访问
    • SecurityContext,:持有Authentication对象和其他可能需要的信息
    • AuthenticationManager:其中可以包含多个AuthenticationProvider
    • ProviderManager:对象为AuthenticationManager接口的实现类
    • AuthenticationProvider:主要用来进行认证操作的类 调用其中的authenticate()方法去进行认证操作
    • Authentication:Spring Security方式的认证主体
    • GrantedAuthority:对认证主题的应用层面的授权,含当前用户的权限信息,通常使用角色表示
    • UserDetails:构建Authentication对象必须的信息,可以自定义,可能需要访问DB得到
    • UserDetailsService:通过username构建UserDetails对象,通过loadUserByUsername根据userName获取UserDetail对象

    1.1.4.常见过滤器

    • WebAsyncManagerIntegrationFilter
    • SecurityContextPersistenceFilter
    • HeaderWriterFilter
    • CorsFilter
    • LogoutFilter
    • RequestCacheAwareFilter
    • SecurityContextHolderAwareRequestFilter
    • AnonymousAuthenticationFilter
    • SessionManagementFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
    • UsernamePasswordAuthenticationFilter
    • BasicAuthenticationFilter

    通常可以继承并重写过滤器,已满足业务要求


    2.集成

    2.1.依赖

        // Spring security
        implementation 'org.springframework.boot:spring-boot-starter-security:2.3.5.RELEASE'
        // JWT
        implementation 'com.auth0:java-jwt:3.11.0'
    

    2.2.准备组件

    仅列出清单,不做赘述,有兴趣可以直接看demo

    • BaseResponse:统一返回结果
    • DataResponse:统一返回数据规范
    • BaseExceptionHandler:异常统一捕获处理器

    2.3.核心组件

    2.3.1.配置安全中心(核心)

    package com.demo.security;
    
    import com.demo.handler.AuthExceptionHandler;
    import com.demo.service.UserService;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.CorsConfigurationSource;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    /**
     * @Description: web安全配置
     * @Author: Echo
     * @Time: 2020/12/8 11:04
     * @Email: 347110596@qq.com
     */
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig {
        /**
         * cors跨域配置
         */
        @Bean
        public CorsConfigurationSource corsConfigurationSource() {
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
            return source;
        }
    
        /**
         * @description: WebMvc配置
         * @author: Echo
         * @email: echo_yezi@qq.com
         * @time: 2020/12/8 11:12
         */
        @Configuration
        public class WebMvcConfig implements WebMvcConfigurer {
            /**
             * 注册cors
             */
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                // 允许路径common下的跨域
                registry.addMapping("/common/**")   // 允许路径
                        .allowCredentials(true) // 不使用cookie故关闭认证
                        .allowedOrigins("*")    // 允许源,设置为全部
                        .allowedMethods("*")    // 允许方法,设置为全部
                        .allowedHeaders("*")    // 允许头,设置为全部
                        .maxAge(3600)   // 缓存时间,设置为1小时
                ;
            }
        }
    
        /**
         * @description: SpringSecurity配置
         * @author: Echo
         * @email: echo_yezi@qq.com
         * @time: 2020/12/8 10:15
         */
        @Configuration
        public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
            private final UserService userService;
    
            WebSecurityConfig(UserService userService) {
                this.userService = userService;
            }
    
            @Override
            protected void configure(AuthenticationManagerBuilder auth) throws Exception {
                // 设置service,需要实现方法loadUserByUsername,用于登录
                auth.userDetailsService(userService);
            }
    
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.cors()     // 开启跨域
                        .and()
                        .csrf().disable()   // 禁用csrf
                        .antMatcher("/**").authorizeRequests()    // 访问拦截
                        .antMatchers("/auth/**").permitAll()  // auth路径下访问放行
                        .antMatchers("/admin/**").hasRole(AccessConstants.ROLE_ADMIN)    // admin路径下限制访问角色
                        .antMatchers("/user/**").hasRole(AccessConstants.ROLE_USER)    // user路径下限制访问角色
                        .anyRequest().authenticated()   // 身份验证
                        .and()
                        .addFilter(new JwtTokenFilter(authenticationManager(), userService))   // 自定义过滤器-jwt
                        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)     // session策略-永不
                        .and()
                        .exceptionHandling()
                        .authenticationEntryPoint(new AuthExceptionHandler())   // 异常处理-认证失败
                        .accessDeniedHandler(new AuthExceptionHandler());   //异常处理-权限错误
            }
        }
    }
    

    核心功能为重写WebSecurityConfigurerAdapter,设置安全配置

    • 重写configure(AuthenticationManagerBuilder auth),设置service,其中service必须继承类UserDetailsService,并需要重写loadUserByUsername方法,用于解析token并组装用户信息
    • 重写configure(HttpSecurity http),设置相关安全策略,并设置一个过滤器

    本配置中还配置了cors跨域,开放了路径/common/**下的跨域,请按照实际需求设置

    这里还可以设置很多东西,这里只介绍一小部分


    2.3.2.自定义token(基于Jwt)

    package com.demo.security;
    
    import lombok.Getter;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.util.Collection;
    
    /**
     * @description: 自定义token
     * @author: Echo
     * @email: echo_yezi@qq.com
     * @time: 2020/12/8 10:05
     */
    public class JwtToken extends UsernamePasswordAuthenticationToken implements UserDetails {
        /**
         * token包括的信息
         */
        @Getter
        private final Type type;
        @Getter
        private final Long userId;
    
        /**
         * token的claim包括的常量key
         */
        public static final String CLAIM_KEY_USER_ID = "userId";
        public static final String CLAIM_KEY_MOBILE = "mobile";
        public static final String CLAIM_KEY_TYPE = "type";
        public static final String CLAIM_KEY_RULES = "rules";
        public static final int TOKEN_EXPIRES_DAYS = 1;
    
        public JwtToken(Type type, Object principal, Long userId, Collection<? extends GrantedAuthority> authorities) {
            // credentials设置为空,身份验证不由jwt管理
            super(principal, null, authorities);
            this.type = type;
            this.userId = userId;
        }
    
        // 枚举type
        public enum Type {
            ADMIN(),
            USER(),
            ;
        }
    
        // 弃用密码,不使用
        @Override
        public String getPassword() {
            return null;
        }
    
        // 获取账号
        @Override
        public String getUsername() {
            return String.valueOf(this.getPrincipal());
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return false;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return false;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return false;
        }
    
        @Override
        public boolean isEnabled() {
            return false;
        }
    }
    

    继承了类UsernamePasswordAuthenticationToken并实现接口UserDetails,用于存储身份信息

    重写了从主题中读取账号和密码的功能,因为密码不参与存储故直接返回空,账号则直接类型转换获取


    2.3.3.token过滤器

    package com.demo.security;
    
    import com.google.common.base.Strings;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Objects;
    
    /**
     * @description: token过滤器,提供给SpringSecurity使用
     * @author: Echo
     * @email: echo_yezi@qq.com
     * @time: 2020/12/8 10:07
     */
    public class JwtTokenFilter extends BasicAuthenticationFilter {
        public static final String TOKEN_HEADER = "Token";
        public static final String TOKEN_SECRET = "e8258f17-b436-4cad-bfc3-2d810ec86238";
    
        private final UserDetailsService userDetailsService;
    
        public JwtTokenFilter(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) {
            super(authenticationManager);
            this.userDetailsService = userDetailsService;
        }
    
        /**
         * AOP,将token中的数据,提取出来放入request作为参数
         */
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
            String token = request.getHeader(TOKEN_HEADER);
            if (!Strings.isNullOrEmpty(token)) {
                JwtToken details = (JwtToken) userDetailsService.loadUserByUsername(token);
                if (Objects.nonNull(details)) {
                    SecurityContextHolder.getContext().setAuthentication(details);
                    request.setAttribute("authId", details.getUserId());
                    request.setAttribute("authType", details.getType());
                    request.setAttribute("auth", details);
                }
            }
            chain.doFilter(request, response);
        }
    }
    

    需要继承类BasicAuthenticationFilter,并重写其内部过滤方法,通过AOP的方式将token中的数据写入request

    之前介绍的纯Jwt认证框架也是使用的AOP来校验token参数和写入参数到request


    2.3.4.认证异常handler

    package com.demo.handler;
    
    import com.demo.response.BaseResponse;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.ObjectWriter;
    import com.google.common.base.Charsets;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.MediaType;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.security.web.access.AccessDeniedHandler;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * @description: 认证异常handler
     * @author: Echo
     * @email: echo_yezi@qq.com
     * @time: 2020/12/8 9:40
     */
    @Slf4j
    public class AuthExceptionHandler implements AccessDeniedHandler, AuthenticationEntryPoint {
        private final ObjectWriter objectWriter = new ObjectMapper().writer().withDefaultPrettyPrinter();
    
        /**
         * 禁止访问
         */
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
            sendError(response, false, accessDeniedException.getLocalizedMessage());
        }
    
        /**
         * 未登录
         */
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
            sendError(response, true, authException.getLocalizedMessage());
        }
    
        private void sendError(HttpServletResponse response, boolean redirectLogin, String message) throws IOException {
            response.setCharacterEncoding(Charsets.UTF_8.displayName());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            log.error("权限错误", message);
            objectWriter.writeValue(response.getWriter(), redirectLogin ? BaseResponse.RESPONSE_NOT_LOGIN : BaseResponse.RESPONSE_AUTH_DENIED);
        }
    }
    

    其实就是同时继承权限拒绝回调和认证拒绝回调,进行统一处理,分开写也是可以的,写一起只是为了省事。。。


    2.3.5.权限鉴别器(核心)

    package com.demo.security;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.dao.DuplicateKeyException;
    import org.springframework.security.access.PermissionEvaluator;
    import org.springframework.security.core.Authentication;
    import org.springframework.stereotype.Component;
    
    import java.io.Serializable;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Objects;
    import java.util.function.BiPredicate;
    
    /**
     * @description: 权限鉴别器
     * @author: Echo
     * @email: echo_yezi@qq.com
     * @time: 2020/12/8 9:28
     */
    @Slf4j
    @Component
    public class GlobalPermissionEvaluator implements PermissionEvaluator {
        /**
         * 自定义断言
         */
        public interface EvalPredicate extends BiPredicate<Object, JwtToken> {
    
        }
    
        /**
         * 权限map,用于存储相关权限
         */
        private final Map<Object, EvalPredicate> userPredicates = new HashMap<>();
        private final Map<Object, EvalPredicate> adminPredicates = new HashMap<>();
    
        /**
         * @description: 检查权限
         * @author: Echo
         * @email: echo_yezi@qq.com
         * @time: 2020/12/8 9:29
         */
        @Override
        public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
            if (!(authentication instanceof JwtToken)) {
                return false;
            }
            // 转换为可识别的 userDetails
            JwtToken userDetails = (JwtToken) authentication;
            Map<Object, EvalPredicate> predicates = getPredicates(userDetails.getType());
            // 检查权限
            boolean pass = false;
            if (predicates.containsKey(permission)) {
                pass = predicates.get(permission).test(targetDomainObject, userDetails);
            } else {
                log.info("reject permission: {} {}", permission, targetDomainObject);
            }
            return pass;
        }
    
        @Override
        public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
            return hasPermission(authentication, targetId, permission);
        }
    
        /**
         * @description: 权限注册
         * @author: Echo
         * @email: echo_yezi@qq.com
         * @time: 2020/12/8 9:38
         */
        public void registerPermission(JwtToken.Type type, Object permission, EvalPredicate predicate) {
            Map<Object, EvalPredicate> predicates = getPredicates(type);
            if (predicates.containsKey(permission)) {
                throw new DuplicateKeyException("Permission handler duplicate");
            }
            predicates.put(permission, predicate);
        }
    
        /**
         * @description: 获取权限
         * @author: Echo
         * @email: echo_yezi@qq.com
         * @time: 2020/12/8 9:38
         */
        private Map<Object, EvalPredicate> getPredicates(JwtToken.Type type) {
            // 获取集合
            Map<Object, EvalPredicate> predicates;
            switch (type) {
                case USER -> predicates = this.userPredicates;
                case ADMIN -> predicates = this.adminPredicates;
                default -> throw new SecurityException("Permission type not supported!");
            }
            return predicates;
    
        }
    }
    

    需要实现接口PermissionEvaluator,实现其权限认证逻辑

    采用方案如下

    • 鉴别器中定义map,用于存储每个类型的权限和对应的断言
    • service中注册时,将所需要的注册的权限类型和对应的断言,注册到鉴别器中
    • 认证权限时,会调用相对应的断言,进行权限测试

    2.3.6.service层

    package com.demo.service;
    
    import com.demo.security.GlobalPermissionEvaluator;
    
    /**
     * @Description:权限service接口层
     * @Author: Echo
     * @Time: 2020/12/8 16:55
     * @Email: 347110596@qq.com
     */
    public interface AccessService {
        /**
         * 注册权限信息
         */
        void initEvaluator(GlobalPermissionEvaluator evaluator);
    
        /**
         * 校验权限信息
         */
        boolean isAccessible(Long userId, Long targetId);
    }
    
    package com.demo.service;
    
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    /**
     * @description: 用户service接口层
     * @author: Echo
     * @email: echo_yezi@qq.com
     * @time: 2020/12/8 10:16
     */
    public interface UserService{
    
        /**
         * 登录
         *
         * @param username 账号
         * @param password 密码
         * @return token
         */
        String login(String username, String password);
    }
    
    package com.demo.service.impl;
    
    
    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.interfaces.DecodedJWT;
    import com.demo.security.AccessConstants;
    import com.demo.security.GlobalPermissionEvaluator;
    import com.demo.security.JwtToken;
    import com.demo.security.JwtTokenFilter;
    import com.demo.service.AccessService;
    import com.demo.service.UserService;
    import com.google.common.base.Preconditions;
    import com.google.common.base.Splitter;
    import com.google.common.base.Strings;
    import com.google.common.collect.Sets;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import java.util.Calendar;
    import java.util.Date;
    import java.util.Set;
    import java.util.stream.Collectors;
    
    /**
     * @description: 用户service实现层
     * @author: Echo
     * @email: echo_yezi@qq.com
     * @time: 2020/12/8 10:16
     */
    @Service
    public class UserServiceImpl implements UserService, AccessService {
        private final Algorithm jwtAlgorithm;
        private final JWTVerifier jwtVerifier;
    
        public UserServiceImpl(GlobalPermissionEvaluator evaluator) {
            this.jwtAlgorithm = Algorithm.HMAC256(JwtTokenFilter.TOKEN_SECRET.getBytes());
            this.jwtVerifier = JWT.require(jwtAlgorithm).build();
            initEvaluator(evaluator);
        }
    
        /**
         * 注册权限信息
         */
        @Override
        public void initEvaluator(GlobalPermissionEvaluator evaluator) {
            // todo 按照实际情况注册权限
            // 注册管理员的更新权限
            evaluator.registerPermission(JwtToken.Type.ADMIN, AccessConstants.ACCESS_UPDATE, (targetId, token) ->
                    this.isAccessible(token.getUserId(), (Long) targetId));
            // 注册用户的更新权限
            evaluator.registerPermission(JwtToken.Type.USER, AccessConstants.ACCESS_UPDATE, (targetId, token) ->
                    this.isAccessible(token.getUserId(), (Long) targetId));
        }
    
        /**
         * 校验权限信息
         */
        @Override
        public boolean isAccessible(Long userId, Long targetId) {
            // todo 从数据库查询校验权限
            Set<Long> accessIds = switch (userId.toString()) {
                case "1" -> Sets.newHashSet(1L, 2L, 3L, 4L);
                case "2" -> Sets.newHashSet(1L, 2L, 3L);
                case "3" -> Sets.newHashSet(1L, 2L);
                case "4" -> Sets.newHashSet(1L);
                default -> Sets.newHashSet();
            };
            return accessIds.contains(targetId);
        }
    
        /**
         * 装载token
         */
        @Override
        public UserDetails loadUserByUsername(String token) throws UsernameNotFoundException {
            try {
                //解析token
                DecodedJWT verify = jwtVerifier.verify(token);
                Long userId = verify.getClaim(JwtToken.CLAIM_KEY_USER_ID).asLong();
                String mobile = verify.getClaim(JwtToken.CLAIM_KEY_MOBILE).asString();
                JwtToken.Type type = verify.getClaim(JwtToken.CLAIM_KEY_TYPE).as(JwtToken.Type.class);
                String rules = Strings.nullToEmpty(verify.getClaim(JwtToken.CLAIM_KEY_RULES).asString());
                Preconditions.checkNotNull(userId);
                Preconditions.checkState(!Strings.isNullOrEmpty(mobile));
                // 获取权限
                //noinspection UnstableApiUsage
                Set<GrantedAuthority> authorities = Splitter.on("|")
                        .splitToStream(rules)
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toSet());
                //组装token todo这里
                return new JwtToken(type, mobile, userId, authorities);
            } catch (Throwable e) {
                throw new UsernameNotFoundException(e.getMessage());
            }
        }
    
        /**
         * 登录
         *
         * @param username 账号
         * @param password 密码
         * @return token
         */
        @Override
        public String login(String username, String password) {
            // 相关参数 todo 正常业务中需要从数据库中查询,这里也未校验密码
            Long userId = null;
            String mobile = null;
            String rules = null;
            JwtToken.Type type = null;
            switch (username) {
                case "111":
                    userId = 1L;
                    mobile = "13411111111";
                    rules = AccessConstants.formatAccess(
                            AccessConstants.ACCESS_FIND,
                            AccessConstants.ACCESS_UPDATE,
                            AccessConstants.ACCESS_DELETE,
                            AccessConstants.ACCESS_INSERT,
                            AccessConstants.ROLE_HEAD + AccessConstants.ROLE_ADMIN
                    );
                    type = JwtToken.Type.ADMIN;
                    break;
                case "222":
                    userId = 2L;
                    mobile = "13422222222";
                    rules = AccessConstants.formatAccess(
                            AccessConstants.ACCESS_FIND,
                            AccessConstants.ACCESS_UPDATE,
                            AccessConstants.ROLE_ADMIN,
                            AccessConstants.ROLE_HEAD + AccessConstants.ROLE_ADMIN
                    );
                    type = JwtToken.Type.ADMIN;
                    break;
                case "333":
                    userId = 3L;
                    mobile = "13433333333";
                    rules = AccessConstants.formatAccess(
                            AccessConstants.ACCESS_FIND,
                            AccessConstants.ACCESS_INSERT,
                            AccessConstants.ROLE_HEAD + AccessConstants.ROLE_USER
                    );
                    type = JwtToken.Type.USER;
                    break;
                case "444":
                    userId = 4L;
                    mobile = "1344444444";
                    rules = AccessConstants.formatAccess(
                            AccessConstants.ACCESS_FIND,
                            AccessConstants.ROLE_HEAD + AccessConstants.ROLE_USER
                    );
                    type = JwtToken.Type.USER;
                    break;
                default:
                    throw new RuntimeException("账号或密码不正确");
            }
            // 过期时间为1天
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(new Date());
            calendar.add(Calendar.DATE, JwtToken.TOKEN_EXPIRES_DAYS);
    
            //组装token
            return JWT.create()
                    .withClaim(JwtToken.CLAIM_KEY_USER_ID, userId)
                    .withClaim(JwtToken.CLAIM_KEY_MOBILE, mobile)
                    .withClaim(JwtToken.CLAIM_KEY_RULES, rules)
                    .withClaim(JwtToken.CLAIM_KEY_TYPE, type.toString())
                    .withIssuer(this.getClass().getSimpleName())
                    .withIssuedAt(new Date())
                    .withExpiresAt(calendar.getTime())
                    .sign(jwtAlgorithm);
        }
    }
    

    分三个功能

    • 实现登录逻辑供controller层接口使用,通过账号密码查询相关信息并生成token

      为作为controller调用的业务层负责的内容,与框架本身无关

    • 实现loadUserByUsername(String token)方法供安全中心使用,通过token解析出相关数据,并转换为UserDetails对象

      为安全中心需要的功能,如果有多个身份验证方法也可以多个类实现该方法,并在安全中心选择合适的进行配置

    • 实现注册权限信息方法和权限校验方法,并在初始化的时候进行注册,权限校验(hasPermission)时调用校验

      为权限校验相关方法,service初始化时进行注册权限并存储校验方法,所有的权限都应该在合适的地方注册并存储权限校验方法


    另一个service

    package com.demo.service.impl;
    
    import com.demo.security.AccessConstants;
    import com.demo.security.GlobalPermissionEvaluator;
    import com.demo.security.JwtToken;
    import com.demo.service.AccessService;
    import com.google.common.collect.Sets;
    import org.springframework.stereotype.Service;
    
    import java.util.Set;
    
    /**
     * @Description:测试service,用于测试其他权限
     * @Author: Echo
     * @Time: 2020/12/8 17:50
     * @Email: 347110596@qq.com
     */
    @Service
    public class TestServiceImpl implements AccessService {
        public TestServiceImpl(GlobalPermissionEvaluator evaluator) {
            initEvaluator(evaluator);
        }
    
        @Override
        public void initEvaluator(GlobalPermissionEvaluator evaluator) {
            // 注册管理员的详情权限
            evaluator.registerPermission(JwtToken.Type.ADMIN, AccessConstants.ACCESS_DETAIL, (targetId, token) ->
                    this.isAccessible(token.getUserId(), (Long) targetId));
        }
    
        @Override
        public boolean isAccessible(Long userId, Long targetId) {
            // todo 从数据库查询校验权限
            Set<Long> accessIds = switch (userId.toString()) {
                case "1" -> Sets.newHashSet(1L, 2L);
                case "2" -> Sets.newHashSet(1L);
                default -> Sets.newHashSet();
            };
            return accessIds.contains(targetId);
        }
    }
    

    2.4.测试controller

    2.4.1.登录

    package com.demo.controller;
    
    import com.demo.service.UserService;
    import com.demo.response.DataResponse;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.*;
    
    @Slf4j
    @RestController("authController")
    @RequestMapping("/auth")
    public class LoginController {
        private final UserService userService;
    
        public LoginController(UserService userService) {
            this.userService = userService;
        }
    
        @PostMapping("login")
        public DataResponse<?> login(
                @RequestParam(name = "username") String username,
                @RequestParam(name = "password") String password) {
            return new DataResponse(userService.login(username, password));
        }
    }
    

    运行结果

    image-20201208180122450

    2.4.2.admin权限

    package com.demo.controller;
    
    import com.demo.response.DataResponse;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @Slf4j
    @RestController("adminTestController")
    @RequestMapping("/admin/test")
    public class AdminTestController {
    
        /**
         * 测试update权限
         */
        @GetMapping("checkLogin")
        @PreAuthorize("hasAuthority(T(com.demo.security.AccessConstants).ACCESS_UPDATE)")
        public DataResponse<?> checkLogin() {
            return new DataResponse("OK");
        }
    
        /**
         * 测试对目标的update权限
         */
        @GetMapping("checkUpdatePermission")
        @PreAuthorize("hasAuthority(T(com.demo.security.AccessConstants).ACCESS_UPDATE)"
                + "&&hasPermission(#targetId,T(com.demo.security.AccessConstants).ACCESS_UPDATE)")
        public DataResponse<?> checkUpdatePermission(Long targetId) {
            return new DataResponse("OK");
        }
    
        /**
         * 测试对目标的detail权限
         */
        @GetMapping("checkDetailPermission")
        @PreAuthorize("hasPermission(#targetId,T(com.demo.security.AccessConstants).ACCESS_DETAIL)")
        public DataResponse<?> checkDetailPermission(Long targetId) {
            return new DataResponse("OK");
        }
    }
    

    运行结果

    token写入headers里面,管理员账号均为111

    image-20201208180534427

    image-20201208180704240 image-20201208180854300

    2.4.3.user权限

    package com.demo.controller;
    
    import com.demo.response.DataResponse;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.web.bind.annotation.*;
    
    @Slf4j
    @RestController("userTestController")
    @RequestMapping("/user/test")
    public class UserTestController {
    
        /**
         * 检查登录状态
         */
        @GetMapping("checkLogin")
        public DataResponse<?> checkLogin() {
            return new DataResponse("OK");
        }
    
        /**
         * 检查update权限
         */
        @GetMapping("checkPermission")
        @PreAuthorize("hasUpdatePermission(#targetId,T(com.demo.security.AccessConstants).ACCESS_UPDATE)")
        public DataResponse<?> checkUpdatePermission(Long targetId) {
            return new DataResponse("OK");
        }
    }
    

    使用之前账号为111的token访问(admin账号)

    image-20201208181345173

    换用账号为444的token访问

    image-20201208181418970

    image-20201208181634032

    测试结果均符合预期


    完整demo地址https://gitee.com/echo_ye/spring-security-demo


    3.小结

    SpringSecurity主要是用注解、aop、重写组件等方法,来对框架进行自定义

    由于SpringSecurityspring生态中重要的一员,不断随着版本更新维护而越来越完善和强大

    SpringSecurity提供了安全策略设置,进而对全局的请求进行拦截和过滤,保证项目的安全性

    SpringSecurity也可以设置诸如cors、crsf等,可以自定义,但默认关闭,需要开启否则会屏蔽设置给springMVC的相同的设置


    4.SpringSecurity与shiro、jwt

    SpringSecurityshirojwt相比之下,更显得完善和强大,一方面能够给开发者更大的自由发挥能力,开发出更符合业务需求的安全框架,但另一方面也略显臃肿

    shiro相当于SpringSecurity的精简版,基本沿用了主体结构,并加以精简和优化,使得使用起来更加方便

    jwt由于其特性,更加轻便和简洁,但能力也更弱,单由jwt只能实现简单的权限校验,不适合用于较大的框架(能力不够&安全不够),因此往往与shiroSpringSecurity等框架进行组合,共同协作来进行优势互补


    后记

    相比之下。。我更愿意用shiro摆平一切。。


    作者:Echo_Ye

    WX:Echo_YeZ

    Email :echo_yezi@qq.com

    个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

  • 相关阅读:
    2015/12/26 十六、 八 、二 进制转十进制
    2015/12/25 ① 图灵测试 ② 安装jdk出现的问题 ③ 配置环境变量
    java如何产生随机数
    二分查找法
    冒泡排序法
    计算阶乘
    九九乘法小练习
    数组循环语句练习
    经典循环例题练习
    如何用循环语句输出一个三角形
  • 原文地址:https://www.cnblogs.com/silent-bug/p/14104874.html
Copyright © 2020-2023  润新知