Spring Security实现登陆认证授权

Posted sw-code

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security实现登陆认证授权相关的知识,希望对你有一定的参考价值。

前端可以根据权限信息控制菜单和页面展示,操作按钮的显示。但这并不够,如果有人拿到了接口,绕过了页面直接操作数据,这是很危险的。所以我们需要在后端也加入权限控制,只有拥有操作权限,该接口才能被授权访问。 在进入Controller方法前判断当前用户是否拥有访问权限,可以通过Filter加AOP的方式实现认证和授权。本次介绍的是成熟的框架:Spring Security。其他框架还有Shiro等。

前端可以根据权限信息控制菜单和页面展示,操作按钮的显示。但这并不够,如果有人拿到了接口,绕过了页面直接操作数据,这是很危险的。所以我们需要在后端也加入权限控制,只有拥有操作权限,该接口才能被授权访问。

在进入Controller方法前判断当前用户是否拥有访问权限,可以通过Filter加AOP的方式实现认证和授权。本次介绍的是成熟的框架:Spring Security。其他框架还有Shiro等。

Spring Security简介

Spring Security的重要核心功能功能是“认证”和“授权”,即用户认证(Authentication)用户授权(Authorization)两部分:

(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求提供用户名和密码,系统通过校验用户名和密码来完成认证过程。

(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,用的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

Spring Security的特点:

  • 和Spring无缝整合
  • 全面的权限控制
  • 专门为Web开发而设计
  • 重量级

Spring Boot出现后,其为Spring Security提供了自动配置方案,可以使用少量的配置来使用Spring Security。如果你的项目是基于Spring Boot的,使用Spring Security无疑是很棒的选择!

Spring Security实现权限

要对Web资源进行保护,最好的办法莫过于Filter

要对方法调用进行保护,最好的方法莫过于AOP

Spring Security进行认证和鉴权的时候就是利用一系列的Filter进行拦截的。

如图所示,一个请求要想访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分就是负责异常处理,橙色部分则是负责授权。经过一系列拦截最终访问到我们的API。

  • FilterSecurityInterceptor:是一个方法级的过滤器,基本位于过滤链的最底部。
  • ExceptionTranslationFilter:是一个异常过滤器,用来处理在认证授权过程中抛出的异常。
  • UsernamePasswordAuthenticationFilter:对/login的POST请求做拦截,校验表单中用户名、密码。

这里我们只需要重点关注两个过滤器即可:UsernamePasswordAuthenticationFilter负责登陆认证,FilterSecurityInterceptor负责权限授权。

说明:Spring Security的核心逻辑全在这一套过滤器中,过滤器里会调用各种组件完成功能,掌握了这些过滤器和组件你就掌握了Spring Security!这个框架的使用方式就是对这些过滤器和组件进行扩展。

用户认证流程

自定义组件

根据认证流程,我们需要自定义以下组件:

  • UserDetails
  • loadUserByUsername
  • passwordEncoder

1、登陆Filter,判断用户名和密码是否正确,生成token

2、认证解析token组件,判断请求头是否有token,如果有认证完成

3、在配置类配置相关认证类

代码实现

完整项目地址:Server | GitHub

依赖

创建一个spring-security模块(module),可以放在项目的common模块下

创建完成,导入相关的Maven依赖

<dependencies>
    <!-- Spring Security依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>

工具类

ResponseUtil

用于写会数据给前端

import com.fasterxml.jackson.databind.ObjectMapper;
import com.swx.common.pojo.R;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ResponseUtil 

    public static void out(HttpServletResponse response, R r) 
        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        try 
            mapper.writeValue(response.getWriter(), r);
         catch (IOException e) 
            throw new RuntimeException(e);
        
    


JwtHelper

package com.swx.common.jwt;

import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;

import java.util.Date;
public class JwtHelper 

    private static long tokenExpiration = 60 * 60 * 1000;
    private static String tokenSignKey = "xxxxxx";

    public static String createToken(Long userId, String username) 
        return Jwts.builder()
                .setSubject("AUTH-USER")
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .claim("userId", userId)
                .claim("username", username)
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compressWith(CompressionCodecs.GZIP)
                .compact();

    

    public static Long getUserId(String token) 
        try 
            if (StringUtils.isEmpty(token)) return null;
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims body = claimsJws.getBody();
            String userId = body.get("userId").toString();
            return Long.parseLong(userId);
         catch (Exception e) 
            e.printStackTrace();
            return null;
        
    

    public static String getUsername(String token) 
        try 
            if (StringUtils.isEmpty(token)) return null;
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims body = claimsJws.getBody();
            return (String) body.get("username");
         catch (Exception e) 
            e.printStackTrace();
            return null;
        
    


自定义UserDetail

继承UserDetail的User,其中sysUser是项目数据库的实体类

import com.swx.model.system.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;
public class CustomUser extends User 

    private SysUser sysUser;

    public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) 
        super(sysUser.getUsername(), sysUser.getPassword(), authorities);
        this.sysUser = sysUser;
    

    public SysUser getSysUser() 
        return sysUser;
    

    public void setSysUser(SysUser sysUser) 
        this.sysUser = sysUser;
    


自定义解码器

用于匹配前端传过来的密码和数据库中的密码是否一致,其中MD5.encrypt是自定义的MD5加密工具

MD5:MD5 | GitHub

import com.swx.common.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder 
    @Override
    public String encode(CharSequence rawPassword) 
        return MD5.encrypt(rawPassword.toString());
    

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) 
        return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
    

自定义UserDetailsService

该类的实现类会查询项目的数据库,根据用户名获取用户信息,包括密码等,用于匹配和授权。

注意要继承org.springframework.security.core.userdetails.UserDetailsService

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
@Component
public interface UserDetailsService extends org.springframework.security.core.userdetails.UserDetailsService 

    /**
     * 根据用户名获取用户对象,获取不到直接抛异常
     */
    @Override
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;


实现该类

该实现可以放到项目的service.impl中,就像项目其他Service的实现类一样

SysUserService:SysUserServiceImpl | GitHub

SysMenuService:SysMenuServiceImpl | GitHub

Permission:Permission | GitHub

import com.swx.auth.service.SysMenuService;
import com.swx.auth.service.SysUserService;
import com.swx.model.system.SysUser;
import com.swx.security.custom.CustomUser;
import com.swx.security.custom.UserDetailsService;
import com.swx.vo.system.Permission;
import org.springframework.security.authentication.DisabledException;
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.ArrayList;
import java.util.Collections;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService 

    private final SysUserService sysUserService;
    private final SysMenuService sysMenuService;

    public UserDetailsServiceImpl(SysUserService sysUserService, SysMenuService sysMenuService) 
        this.sysUserService = sysUserService;
        this.sysMenuService = sysMenuService;
    

    /**
     * 根据用户名获取用户对象,获取不到直接抛异常
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
        // 根据用户名查询
        SysUser sysUser = sysUserService.getUserByUsername(username);
        if (null == sysUser) 
            throw new UsernameNotFoundException("用户名不存在!");
        
        if (sysUser.getStatus() == 0) 
            throw new DisabledException("disable");
        

        // 查询权限列表
        List<Permission> permissions = sysMenuService.queryUserAuthListByUserId(sysUser.getId());
        // 封装Spring Security的权限类型
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        permissions.forEach(permission -> 
            authorities.add(new SimpleGrantedAuthority(permission.getAuth().trim()));
        );
        return new CustomUser(sysUser, authorities);
    

拦截器

TokenLoginFilter

获得输入的用户名和密码,封装成框架要求的对象,调用认证方法。认证成功则将权限信息存入Redis,并返回Token给前端。

该类继承UsernamePasswordAuthenticationFilter,实现登陆的拦截校验。

import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.swx.common.jwt.JwtHelper;
import com.swx.common.pojo.R;
import com.swx.common.pojo.ResultCode;
import com.swx.common.utils.ResponseUtil;
import com.swx.security.custom.CustomUser;
import com.swx.vo.system.LoginVo;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

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.HashMap;
/**
 * 获得输入的用户名和密码,封装成框架要求的对象,调用认证方法
 */
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter 

    private final RedisTemplate<String, String> redisTemplate;
    // 构造方法
    public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate<String, String> redisTemplate) 
        this.redisTemplate = redisTemplate;
        this.setAuthenticationManager(authenticationManager);
        this.setPostOnly(false);
        // 指定登陆接口及提交方式,可以指定任意路径
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login", "POST"));
    

    // 登陆认证
    // 获取输入的用户名和密码,调用方法认证
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException 
        try 
            // 获取用户信息
            LoginVo loginVo = new ObjectMapper().readValue(request.getInputStream(), LoginVo.class);
            // 封装对象
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
            // 调用方法
            return this.getAuthenticationManager().authenticate(authenticationToken);
         catch (IOException e) 
            throw new RuntimeException(e);
        
    

    // 认证成功调用的方法
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException 
        // 获取当前用户
        CustomUser customUser = (CustomUser) authResult.getPrincipal();
        // 生成Token
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
        // 获取当前用户的权限数据,放到Redis中,key: username  value: permissions
        redisTemplate.opsForValue().set(
                customUser.getUsername(),
                JSON.toJSONString(customUser.getAuthorities()));
        // 返回
        HashMap<String, Object> map = new HashMap<>();
        map.put("token", token);
        ResponseUtil.out(response, R.success(map));
    

    // 认证失败调用的方法
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException 
      	// 封装错误信息,用于返回
        R r = R.fail(ResultCode.LOGIN_AUTH_FAIL);
        Throwable ex = failed.getCause();
        if (ex instanceof DisabledException) 
            r.setResultCode(ResultCode.USER_DISABLE);
         else if (failed instanceof UsernameNotFoundException || failed instanceof BadCredentialsException) 
            r.setResultCode(ResultCode.USER_LOGIN_ERROR);
        
        ResponseUtil.out(response, r);
    

TokenAuthenticationFilter

判断是否完成认证,将认证信息保存到Security上下文中

import com.alibaba.fastjson2.JSON;
import com.swx.common.jwt.JwtHelper;
import com.swx.common.pojo.R;
import com.swx.common.pojo.ResultCode;
import com.swx.common.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
 * 判断是否完成认证
 */
public class TokenAuthenticationFilter extends OncePerRequestFilter 

    private final RedisTemplate<String, String> redisTemplate;

    public TokenAuthenticationFilter(RedisTemplate<String, String> redisTemplate) 
        this.redisTemplate = redisTemplate;
    

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException 
        // 如果是登陆接口,直接放行
        if ("/admin/system/index/login".equals(request.getRequestURI())) 
            chain.doFilter(request, response);
            return;
        

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        if (null != authentication) 
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
         else 
            ResponseUtil.out(response, R.fail(ResultCode.LOGIN_AUTH_FAIL));
        

    

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) 
        String token = request.getHeader("Authorization");
        if (!StringUtils.isEmpty(token)) 
            String username = JwtHelper.getUsername(token);
            if (!StringUtils.isEmpty(username)) 
                // 从redis中获取权限数据
                String authString = redisTemplate.opsForValue().get(username);
                if (!StringUtils.isEmpty(authString)) 
                    List<Map> mapList = JSON.parseArray(authString, Map.class);
                    System.out.println(mapList);
                    List<SimpleGrantedAuthority> authorities = new ArrayList<>();
                    mapList.forEach(map -> 
                        String authority = (String) map.get("authority");
                        authorities.add(new SimpleGrantedAuthority(authority));
                    );
                    return new UsernamePasswordAuthenticationToken(username, null, authorities);
                 else 
                    return new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList());
                
            
        
        return null;
    


配置文件

创建一个Spring Security的配置文件,开启相关的注解

import com.swx.security.custom.CustomMd5PasswordEncoder;
import com.swx.security.filter.TokenAuthenticationFilter;
import com.swx.security.filter.TokenLoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
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.builders.WebSecurity;
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.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 
    private final UserDetailsService userDetailsService;

    private final CustomMd5PasswordEncoder customMd5PasswordEncoder;

    private final RedisTemplate<String, String> redisTemplate;

    public WebSecurityConfig(UserDetailsService userDetailsService, CustomMd5PasswordEncoder customMd5PasswordEncoder, RedisTemplate<String, String> redisTemplate) 
        this.userDetailsService = userDetailsService;
        this.customMd5PasswordEncoder = customMd5PasswordEncoder;
        this.redisTemplate = redisTemplate;
    

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception 
        return super.authenticationManagerBean();
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                .antMatchers("/admin/system/index/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new TokenAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
                .addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate));
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
    

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder);
    

    @Override
    public void configure(WebSecurity web) throws Exception 
        web.ignoring().antMatchers("/favicon.icon", "/swagger-resources/**", "webjars/**", "/v2/**", "swagger-ui.html/**", "doc.html");
    

食用教程

可以在业务模块中导入pom信息

<dependency>
    <groupId>com.swx</groupId>
    <artifactId>spring-security</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

在需要授权的接口上加入注解,就像这样

@Api(tags = "角色管理接口")
@RestController
@ResponseResult
@RequestMapping("/admin/system/sysRole")
public class SysRoleController 

    private final SysRoleService sysRoleService;

    public SysRoleController(SysRoleService sysRoleService) 
        this.sysRoleService = sysRoleService;
    

    @ApiOperation("为用户分配角色")
    @PreAuthorize("hasAuthority(\'system_role_assign\')")
    @PostMapping("/doAssign")
    public void doAssign(@RequestBody AssignRoleVo assignRoleVo) 
        sysRoleService.doAssign(assignRoleVo);
    

    @ApiOperation("查询所有角色")
    @PreAuthorize("hasAuthority(\'system_role_list\')")
    @GetMapping("/findAll")
    public List<SysRole> findAll() 
        return sysRoleService.list();
    

    /**
     *
     * @param page  当前页
     * @param limit 记录数
     * @param sysRoleQueryVo 查询参数
     * @return 分页信息
     */
    @ApiOperation("条件分页查询")
    @PreAuthorize("hasAuthority(\'system_role_list\')")
    @GetMapping("page/limit")
    public IPage<SysRole> pageQueryRole(@PathVariable Long page,
                                        @PathVariable Long limit,
                                       SysRoleQueryVo sysRoleQueryVo) 
        // 自定义Page,修改current为page,和前端保持一致
        CustomPage<SysRole> pageParam = new CustomPage<>(page, limit);
        LambdaQueryWrapper<SysRole> wrapper = new LambdaQueryWrapper<>();
        String roleName = sysRoleQueryVo.getRoleName();
        if (!StringUtils.isEmpty(roleName)) 
            wrapper.like(SysRole::getRoleName, roleName);
        
        IPage<SysRole> iPage = sysRoleService.page(pageParam, wrapper);
        return iPage;
    

    @ApiOperation("添加角色")
    @PreAuthorize("hasAuthority(\'system_role_add\')")
    @PostMapping("")
    public void save(@RequestBody SysRole role) 
        boolean save = sysRoleService.save(role);
        if (!save) 
            throw new BizException("添加失败");
        
    

    /**
     * 根据id查询角色
     * @param id 角色id
     * @return 角色
     */
    @ApiOperation("根据ID查询")
    @PreAuthorize("hasAuthority(\'system_role_list\')")
    @GetMapping("id")
    public SysRole get(@PathVariable Long id) 
        return sysRoleService.getById(id);
    

    /**
     * 更新角色
     * @param role 角色信息
     */
    @ApiOperation("更新角色")
    @PreAuthorize("hasAuthority(\'system_role_update\')")
    @PutMapping("")
    public void update(@RequestBody SysRole role) 
        boolean update = sysRoleService.updateById(role);
        if (!update) 
            throw new BizException("更新失败");
        
    

    @ApiOperation("根据id删除")
    @PreAuthorize("hasAuthority(\'system_role_remove\')")
    @DeleteMapping("id")
    public void delete(@PathVariable Long id) 
        boolean delete = sysRoleService.removeById(id);
        if (!delete) 
            throw new BizException("删除失败");
        
    

    @ApiOperation("批量删除")
    @PreAuthorize("hasAuthority(\'system_role_remove\')")
    @DeleteMapping("batch")
    public void batchRemove(@RequestBody List<Long> ids) 
        boolean delete = sysRoleService.removeByIds(ids);
        if (!delete) 
            throw new BizException("删除失败");
        
    


Spring Security4实战与原理分析视频课程( 扩展+自定义)

Spring Security概述与课程概要介绍


Spring Security快速入门(基于XML)

Spring Security快速入门(基于XML)

URL匹配详解

自定义登陆

配置退出

Ajax登陆退出

JDBC认证

层级角色关系

认证体系介绍

自定义认证

匿名认证

认证流程分析

配置权限

授权体系介绍

自定义授权

自定义JDBC授权

表达式权限原理分析

表达式权限扩展

自定义异常处理

过滤器分析

过滤器应用

FilterChainProxy初始化流程分析

授权流程分析

Spring Security运行流程分析

整合Spring Boot


视频教程地址:http://edu.51cto.com/course/course_id-8615.html


本文出自 “我的博客” 博客,转载请与作者联系!

以上是关于Spring Security实现登陆认证授权的主要内容,如果未能解决你的问题,请参考以下文章

Spring security 和 AOP 学习

Spring Security4实战与原理分析视频课程( 扩展+自定义)

Spring Security4实战与原理分析视频课程( 扩展+自定义)

Spring Security(新版本)实现权限认证与授权

Spring Security + JJWT 实现 JWT 认证和授权

Spring Security源码(一):认证、授权、过滤器链