SpringBoot整合SpringSecurity+JWT

1,032 阅读6分钟

1,快速入门

1.1 搭建springboot项目

image.png

1.2 导 SpringSecurity依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

导入依赖后,再启动项目,security会自动拦截访问的接口。用户名:user;密码会在控制台中打印出来。

2,认证

2.1 登录校验流程

image.png

2.1 原理初探

2.2.1 SpringSecurity 完整流程

SpringSecurity的原理其实是一个过滤器链,内部包含各种功能过滤器。下图为引入security依赖后自带的过滤器。

image.png

  • UsernamePasswordAuthenticationFilter:对用户名密码的过滤。
  • ExceptionTranslationFilter:对异常的捕获处理。
  • FilterSecurityInterceptor:权限校验过滤器。

image.png

2.2.2 认证流程详解

image.png

登录接口 image.png

其他接口

image.png

2.3 解决问题

2.3.1 思路分析

登录:

  • 自定义登录接口

    • 调用ProviderManager的方法进行认证,如果认证通过生成jwt,把用户信息存入redis中
  • 自定义UserDetailsService

    • 在这个实现查询数据库

校验:

  • 定义jwt认证过滤器

    • 获取token
    • 解析token,获取其中userid
    • 从redis中获取用户信息
    • 存入securityContextHolder中

2.3.2 准备工作

配置redis

添加依赖

<!--redis依赖配置-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

添加配置类

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        //key采用String的序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        RedisSerializer<Object> serializer = redisSerializer();

        // value序列化方式采用jackson
        redisTemplate.setValueSerializer(serializer);
        // hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(serializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisSerializer<Object> redisSerializer(){
        //创建json序列化器
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //必须设置,否则无法将json转化为对象,会转化为map类型
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(objectMapper);
        return serializer;
    }

}

改yml

spring:
  redis:
    host: 192.168.0.238
    port: 6379

封装返回结果

public class CommonResult<T>{

    private long code;
    private String message;
    private T data;

    protected CommonResult() {
    }

    public CommonResult(long code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    /**
     * 成功返回结果
     * @param data 获取的数据
     * @return
     */
    public static <T> CommonResult<T> success(T data){
        return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
    }

    /**
     * 成功返回结果
     * @param message 提示信息
     * @param data 获取的数据
     * @return
     */
    public static <T> CommonResult<T> success(String message,T data){
        return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
    }

    /**
     * 失败返回结果
     */
    public static <T> CommonResult<T> failed(){
        return new CommonResult<T>(ResultCode.FAILED.getCode(), ResultCode.FAILED.getMessage(), null);
    }

    /**
     * 失败返回结果
     * @param message 提示信息
     * @return
     */
    public static <T> CommonResult<T> failed(String message){
        return new CommonResult<>(ResultCode.FAILED.getCode(), message,null);
    }

    /**
     * 失败返回结果
     * @param errorCode 错误码
     * @return
     */
    public static <T> CommonResult<T> failed(IErrorCode errorCode){
        return new CommonResult<>(errorCode.getCode(),errorCode.getMessage(),null);
    }

    /**
     * 失败返回结果
     * @param errorCode 错误码
     * @param message 错误信息
     */
    public static <T> CommonResult<T> failed(IErrorCode errorCode, String message) {
        return new CommonResult<T>(errorCode.getCode(), message, null);
    }

    /**
     * 参数验证失败返回结果
     */
    public static <T> CommonResult<T> validateFailed() {
        return failed(ResultCode.VALIDATE_FAILED);
    }

    /**
     * 参数验证失败返回结果
     * @param message 提示信息
     */
    public static <T> CommonResult<T> validateFailed(String message) {
        return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
    }

    /**
     * 未登录返回结果
     */
    public static <T> CommonResult<T> unauthorized(T data) {
        return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
    }

    /**
     * 未授权返回结果
     */
    public static <T> CommonResult<T> forbidden(T data) {
        return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
    }

    public long getCode() {
        return code;
    }

    public void setCode(long code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
public enum ResultCode implements IErrorCode {

    SUCCESS(200,"操作成功"),
    FAILED(500,"操作失败"),
    VALIDATE_FAILED(404,"参数检验失败"),
    UNAUTHORIZED(401,"没登录或token过期"),
    FORBIDDEN(403,"没有相关权限");

    private long code;
    private String message;

    private ResultCode(long code,String message){
        this.code = code;
        this.message = message;
    }


    @Override
    public long getCode() {
        return code;
    }

    @Override
    public String getMessage() {
        return message;
    }
}
public interface IErrorCode {

    long getCode();

    String getMessage();
}

引入jwt

引依赖

<!--JWT(Json Web Token)登录支持-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

工具类

2.3.3 实现

2.3.3.1 数据库校验用户

数据库配置

导入数据库依赖

        <!-- mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!-- mybatis-plus-generator -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!--Velocity模板引擎  mybatis-plus -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.3</version>
        </dependency>
        <!--Mysql数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>
        <!--集成druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.9</version>
        </dependency>

mybatis-plus配置

@Configuration
public class MybatisPlusConfig {
    /**
     * 分页插件
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

代码生成器配置

public class MybatisPlusGenerator {
    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotEmpty(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
        //获取程序当前路径
        String projectPath =  System.getProperty("user.dir");
//        String projectPath = "D:\java测试目录";
        // 数据源配置
        String dbUrl = "jdbc:mysql://localhost:3306/mall_tiny?characterEncoding=UTF-8&useUnicode=true&useSSL=false";
        DataSourceConfig.Builder dataSourceConfig = new DataSourceConfig.Builder(dbUrl,"root","12345")
                .dbQuery(new MySqlQuery())
                .typeConvert(new MySqlTypeConvert())
                .keyWordsHandler(new MySqlKeyWordsHandler());

        // 代码生成器
        FastAutoGenerator mpg = FastAutoGenerator.create(dataSourceConfig);
        mpg.globalConfig(globalBuilder -> globalBuilder.fileOverride().disableOpenDir()
                .outputDir(projectPath + "/src/main/java")
                .author("")
                .commentDate("yyyy-MM-dd HH:mm:ss")
                .dateType(DateType.TIME_PACK)
                .enableSwagger());
        mpg.packageConfig(packageBuilder -> packageBuilder
                .parent("com.xybian.mall.tiny")
                .service("services")
                .serviceImpl("services.impl")
                .xml("mybatis")
        );



        mpg.strategyConfig(strategyconfigBuilder -> strategyconfigBuilder
                .enableCapitalMode()
                .enableSkipView ()
                .disableSqlFilter()
                .addInclude(scanner("表名"))
        );



        //entity 生成策略
        mpg.strategyConfig(strategyconfigBuilder -> strategyconfigBuilder.entityBuilder()
//                      .enableTableFieldAnnotation ()
                        .naming (NamingStrategy.underline_to_camel)
                        .columnNaming (NamingStrategy.underline_to_camel)
                        .idType(IdType.AUTO)
//                      .enableLombok ()
                        /*.logicDeleteColumnName ( "deleted ").logicDeletePropertyName ( "deleted ")
                        .addTableFills(new Column( "create_time" , FieldFill.INSERT))
                        .addTableFills(new Property( "updateTime " ,FieldFill.INSERT_UPDATE))*/
                        .versionColumnName ("version")
                        .disableSerialVersionUID()
        );

        //controller 生成策略
        mpg.strategyConfig(
                strategyconfigBuilder ->strategyconfigBuilder
                        .controllerBuilder()
                        .enableRestStyle()
                        .enableHyphenStyle()
        );


        //service 生成策略
        mpg.strategyConfig(
                strategyconfigBuilder -> strategyconfigBuilder
                        .serviceBuilder()
                        .formatServiceFileName ("%sService")
                        .formatServiceImplFileName ("%sServiceImpl"));

        //mapper 生成策略
        mpg.strategyConfig (
                strategyconfigBuilder ->strategyconfigBuilder.mapperBuilder()
                        .formatMapperFileName ("%sMapper")
                        .formatXmlFileName ( "%sMapper" )
                        .enableBaseResultMap());

        mpg.execute();
    }
}

改yml

spring:
  redis:
    host: 192.168.0.238
    port: 6379


  datasource:
    druid:
      url: jdbc:mysql://localhost:3306/sys_secutiry?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
      username: root
      password: 12345
      driver-class-name: com.mysql.cj.jdbc.Driver


#mybatis plus 设置
mybatis-plus:
  mapper-locations: classpath:/mybatis/*Mapper.xml
  global-config:
    db-config:
      id-type: auto
  configuration:
    auto-mapping-behavior: partial
    map-underscore-to-camel-case: true

添加包扫描

image.png

添加controller,mapper,service 并测试

image.png

校验配置

有上面讲解的认证流程,我们可以知道,主要是修改 UserDetailsService 这个接口的实现类,来实现从数据库去对 用户名和密码 的校验。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //从数据库查询校验
        LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUser::getUserName,username);
        SysUser sysUser = sysUserService.getOne(wrapper);

        if (sysUser == null){
            throw new UsernameNotFoundException("用户不存在");
        }

        //授权

        //返回

        return new UserDetailsImpl(sysUser);
    }
}

因为上面的返回,需要为UserDetails接口,所以在定义一个UserDetailsImpl类,实现接口,去返回数据。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDetailsImpl implements UserDetails {

    private SysUser sysUser;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }

    @Override
    public String getUsername() {
        return sysUser.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

以上修改完成后,就可以做到从数据库去认证用户。

SpringSecurity密码校验机制:数据库存储的密码格式为 {加密方式}password 。校验时,会先从数据库读取密码,获取密码前的 { } 中的加密方式,然后用这个加密方式对登录时的密码加密,再和数据库密码对比。

注:{noop} 表示不加密,按明文显示。

2.3.3.2 密码加密存储

修改SpringSecurity默认的加密方式,改成 BCryptPasswordEncoder

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //创建 BCryptPasswordEncoder 注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

2.3.3.3 登录接口

先自定义一个登录接口

@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;

    @PostMapping("/user/login")
    public CommonResult login(@RequestBody SysUser user) throws AuthenticationException {

        CommonResult result = loginService.login(user);

        return result;
    }
}

登录方法的实现

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public CommonResult login(SysUser user) throws AuthenticationException {


    //封装 Authentication
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());

    //认证用户
    Authentication authenticate = authenticationManager.authenticate(authenticationToken);

    if (authenticate == null){
        throw new AuthenticationException("认证失败");
    }

    UserDetailsImpl userDetails = (UserDetailsImpl)authenticate.getPrincipal();
    SysUser sysUser = userDetails.getSysUser();


    //认证通过,生成jwt
    Map<String, Object> map = new HashMap<>();
    map.put("userId",sysUser.getId());
    String token = jwtTokenUtil.generateToken(map);

    //用户信息存入 redis 中
    redisTemplate.opsForValue().set("user-"+sysUser.getId(),sysUser);

    //将token信息返回
    Map<String, Object> dataMap = new HashMap<>();
    dataMap.put("token",token);
    return CommonResult.success(dataMap);

    }
}

securityConfig配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //创建 BCryptPasswordEncoder 注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    //为登录接口 提供下一步 调用的方法
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //放行登录方法
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭 csrf
                .csrf().disable()
                //不通过 session 获取 SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //对登录接口  允许匿名访问  /user/login
                .antMatchers("/user/login").anonymous()
                //除上面外的所有请求全部需要 鉴权认证
                .anyRequest().authenticated();
    }
}

接口测试

image.png

2.3.3.4 认证过滤器

添加一个对token验证的过滤器,同时设置该过滤器在 UsernamePasswordAuthenticationFilter 之前运行,这个保证先验证token,在验证用户登录。

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenUtil tokenUtil;

    @Autowired
    private RedisTemplate redisTemplate;

    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 获取 token
        String token = httpServletRequest.getHeader("token");
        if (!StringUtils.hasText(token)){

            filterChain.doFilter(httpServletRequest,httpServletResponse);

            return;
        }
        // 解析token,获取 userId

        Claims claims = tokenUtil.getClaimsFromToken(token);

        String userId = claims.get("userId").toString();

        //从redis中获取User
        Object o = redisTemplate.opsForValue().get("user-" + userId);

        if (ObjectUtils.isEmpty(o)){
            throw new AuthenticationException("没有用户信息");
        }

        SysUser user = (SysUser)o;

        //将User 封装到 securityContextHolder。 封装到securityContextHolder以后,其他过滤器就不会在拦截
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

securityConfig中的配置

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            //关闭 csrf
            .csrf().disable()
            //不通过 session 获取 SecurityContext
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            //对登录接口  允许匿名访问  /user/login
            .antMatchers("/user/login").anonymous()
            //除上面外的所有请求全部需要 鉴权认证
            .anyRequest().authenticated();


    //定义filter的先后顺序,保证 jwtFilter比用户验证的过滤器先执行
    http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
}

2.3.3.5 退出登录

因为每一次请求都是一个新的 securityContextHolder,所以将 redis 中用户信息删除后,在用token访问接口时,在获取 redis 用户信息时,无法通过,即注销成功。

@GetMapping("/user/logout")
public CommonResult logout(){
    return loginService.logout();
}
@Override
public CommonResult logout() {
    //获取 securityContextHolder
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    SysUser user = (SysUser)authentication.getPrincipal();


    //删除redis中的用户信息
    redisTemplate.delete("user-"+user.getId());

    return CommonResult.success("注销成功",null);
}

3,授权

1,开启权限注解

image.png

2,在需要设置权限的接口上加上权限限制

image.png

3,在做登录用户校验的时候,从数据库查询设置该用户的权限,本测试先写死。

image.png

image.png

4,对token过滤器修改

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenUtil tokenUtil;

    @Autowired
    private RedisTemplate redisTemplate;

    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 获取 token
        String token = httpServletRequest.getHeader("token");
        if (!StringUtils.hasText(token)){

            filterChain.doFilter(httpServletRequest,httpServletResponse);

            return;
        }
        // 解析token,获取 userId

        Claims claims = tokenUtil.getClaimsFromToken(token);

        String userId = claims.get("userId").toString();

        //从redis中获取User
        Object o = redisTemplate.opsForValue().get("user-" + userId);

        if (ObjectUtils.isEmpty(o)){
            throw new AuthenticationException("没有用户信息");
        }

        SysUser user = (SysUser)o;

        //用户权限信息
        List<SimpleGrantedAuthority> authorityList = user.getPermissions().stream().map(permission -> new SimpleGrantedAuthority(permission)).collect(Collectors.toList());

        //将User 封装到 securityContextHolder。 封装到securityContextHolder以后,其他过滤器就不会在拦截
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,authorityList);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

RBAC权限模型

image.png

4,自定义失败提示

实现这个两个接口。再将这两个实现类添加到securityConfig的配置中。

AuthenticationEntryPoint 认证异常

AccessDeineHandler 鉴权异常

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        Map<String, Object> map = new HashMap<>();
        map.put("uri",httpServletRequest.getRequestURI());
        map.put("msg","鉴权失败");
        httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ObjectMapper objectMapper = new ObjectMapper();
        String resBody = objectMapper.writeValueAsString(map);
        PrintWriter printWriter = httpServletResponse.getWriter();
        printWriter.print(resBody);
        printWriter.flush();
        printWriter.close();
    }
}
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {

        Map<String, Object> map = new HashMap<>();
        map.put("uri",httpServletRequest.getRequestURI());
        map.put("msg","认证失败");
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ObjectMapper objectMapper = new ObjectMapper();
        String resBody = objectMapper.writeValueAsString(map);
        PrintWriter printWriter = httpServletResponse.getWriter();
        printWriter.print(resBody);
        printWriter.flush();
        printWriter.close();
    }
}

5,跨域

浏览器出于安全的考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

先在springboot中配置

image.png

再在springsecurity中配置

image.png

6,补充

6.1 自定义权限校验方法

/**
 * 自定义权限校验方法
 */
@Component
public class AuthenticationExpression {
    
    public boolean hasTrue(String permission){
        
        //从 securityContextHolder中获取用户权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        UserDetailsImpl userDetails = (UserDetailsImpl)authentication.getPrincipal();
        
        //权限信息
        List<String> permissions = userDetails.getPermissions();

        //判断是否有权限
        return permissions.contains(permission);
    }
}

image.png

6.2,基于配置设置设置权限

image.png

6.3, CSRF

CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。

blog.csdn.net/freeking101…

SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。

后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。

我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。

6.4 处理器

认证成功处理器

认证失败处理器

登录成功处理器

7,代码地址

gitee地址:gitee.com/xinyunbian/…

8,参考讲解