【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

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

posted @ 2020-12-08 18:35  Echo_Ye  阅读(161)  评论(0编辑  收藏  举报