本文最后更新于:2023年8月24日 晚上
SpringSecurity介绍 spring security是spring家族中的一个安全管理框架,相对与另外一个安全框架shiro,提供了更丰富的功能(shiro上手更加简单)
一般web应用都需要进行认证和授权:
认证(验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户)
授权(经过认证后判断当前哟怒是否有权限进行某个操作)
而认证和授权也是spring security作为安全框架的核心功能。
请求到security内部执行的流程 spring security的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器(Filter)。
请求过程: 请求 —> 经过UsernamePasswordAuthenticationFilter—> 经过ExceptionTranslationFilter —> 经过FilterSecurityInterceptor —> API
UsernamePasswordAuthenticationFilter(表单登录过滤器)
负责处理我们再登录页面填写了用户名密码后的登录请求。
ExceptionTranslationFilter(统一异常处理过滤器)
处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException.
FilterSecurityInterceptor(过滤器链中最后一个过滤器,负责权限校验)
获取当前request对应的权限配置
调用访问控制器进行鉴权操作等核心功能
响应过程 响应也是需要再反过来走一遍过滤器的
APi —> 经过FilterSecurityInterceptor —> 经过ExceptionTranslationFilter —> 经过UsernamePasswordAuthenticationFilter —> 响应
思路分析 登录:
自定义登录接口
调用ProviderManager的方法进行认证,通过则生成jwt返回
把用户信息存入redis(先序列化redis)
自定义类实现UserDetailsService,将认证过程改为数据库查询(配合LambdaQueryWrapper)
ProviderManager 是 spring security提供的AuthenticationManager实现。其主要目的,也就是实现AuthenticationManager接口所定义的方法。
序列化redis:使用fastjson修改序列化redis,使其存入和取出的样式统一,具体代码可以cv
LambdaQueryWrapper:mybatis-plus中使用lambda表达式写条件查询
校验:
定义jwt认证过滤器
获取token
解析token获取其中的userId
使用userId从redis中查询用户信息
存入SecurityContextHolder供后续过滤器调用
准备工作
启动springboot项目,引入redis,security
导入redis工具类(见redis使用fastjson序列化)
导入jwt和对应工具类(见jjwt工具类)
导入mybatis-plus
配置yml连接mysql和redis
在yml修改security用户名和密码
数据库校验用户 数据库准备 创建RBAC数据库结构,写对应的实体类(使用@TableName可以mybatis-plus映射指定数据表,并不需要表名一致)
写一个类实现UserDetailsService,重写loadUserByUsername 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.fsan.springsecurity.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.fsan.springsecurity.mapper.SysUserMapper;import com.fsan.springsecurity.pojo.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Service;import java.util.Objects;@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysUserMapper sysUserMapper; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(User::getUserName, username); User user = sysUserMapper.selectOne(queryWrapper); if (Objects.isNull(user)){ throw new RuntimeException ("用户名或者密码错误!" ); } } }
这是使用的条件包装器为LambdaQueryWrapper,并不是传统的QueryWrapper,LambdaQueryWrapper更好用
使用 Objects.isNull(user) 判断对象是否为空
QueryWrapper到LambdaQueryWrapper的演变见 https://blog.csdn.net/qlzw1990/article/details/116996422
因为loadUserByUsername返回的是UserDetails类型数据,创建一个实体类实现UserDetails,将所有方法重写,并改为返回true
创建一个实体类实现UserDetails 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package com.fsan.springsecurity.pojo;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;@Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails { private User user; @Override public Collection<? extends GrantedAuthority > getAuthorities() { return null ; } @Override public String getPassword () { return user.getPassword(); } @Override public String getUsername () { return user.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 ; } }
这里的getUsername和getPassword要改为User实体类的提供的方法,否则数据存入之后,调用getUsername拿取不到这里的getUsername和getPassword要改为User实体类的提供的方法,否则数据存入之后,调用getUsername拿取不到
在loadUserByUsername中调用mybatis-plus查询数据库,返回的类型为UserDetails对象,但是LoginUser已经对UserDetails类重写了,所以直接返回LoginUser对象即可,直接传入查询到的User类型的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.fsan.springsecurity.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.fsan.springsecurity.mapper.SysUserMapper;import com.fsan.springsecurity.pojo.LoginUser;import com.fsan.springsecurity.pojo.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Service;import java.util.Objects;@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysUserMapper sysUserMapper; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(User::getUserName, username); User user = sysUserMapper.selectOne(queryWrapper); if (Objects.isNull(user)){ throw new RuntimeException ("用户名或者密码错误!" ); } return new LoginUser (user); } }
密码加密校验 返回的UserDetails对象会经过权限过滤器,在权限过滤器中判断输入的用户名,密码和UserDetails中存储的用户名密码,但是UserDetails对象中默认的密码格式为:{id}password,security会根据id判断密码的加密方式,我们一般不会采用默认的方式,所以就需要将PasswordEncoder替换为BCryptPasswordEncoder
测试BCryptPasswordEncoder类的加密和判断: 1 2 3 4 5 new BCryptPasswordEncoder ().encode("FSAN" ) new BCryptPasswordEncoder ().matches("FSAN" , "$2a$10$qNin1NOP265Zqme378882uxos.haI3Da9JCi3gPg65GOHb/5t1oGO" )
对一个字符串,重复使用加密后也是不同的,内部是随机加盐生成的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.fsan.springsecurity.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder (); } }
也算是使用@Bean修改passwordEncoder
直接@Autowired注入使用passwordEncoder.encode(“FSAN”)
security配置
关闭csrf
不使用session获取SecurityContent
配置不登录就可以访问的接口(匿名登陆)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override protected void configure (HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/login" ).anonymous() .anyRequest().authenticated(); }
注入AuthenticationManager权限管理 1 2 3 4 5 6 7 8 9 10 11 @Bean @Override public AuthenticationManager authenticationManagerBean () throws Exception { return super .authenticationManagerBean(); }
写登录的controller和service controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.fsan.springsecurity.controller;import com.fsan.springsecurity.pojo.ResponseResult;import com.fsan.springsecurity.pojo.User;import com.fsan.springsecurity.service.impl.LoginServiceImpl;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;@RestController public class LoginController { @Autowired private LoginServiceImpl loginService; @PostMapping("/user/login") public ResponseResult login (@RequestBody User user) { return loginService.login(user); } }
service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.fsan.springsecurity.service.impl;import com.fsan.springsecurity.Utils.JwtUtil;import com.fsan.springsecurity.pojo.LoginUser;import com.fsan.springsecurity.pojo.ResponseResult;import com.fsan.springsecurity.pojo.User;import com.fsan.springsecurity.service.LoginService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.Authentication;import org.springframework.stereotype.Service;import java.util.HashMap;import java.util.Objects;@Service public class LoginServiceImpl implements LoginService { @Autowired private AuthenticationManager authenticationManager; @Override public ResponseResult login (User user) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken (user.getUserName(), user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); if (Objects.isNull(authenticate)) { throw new RuntimeException ("登录失败!" ); } LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userId = loginUser.getUser().getId().toString(); String token = JwtUtil.createJWT("token" , 10 , userId); HashMap<String, String> dataMap = new HashMap <>(); dataMap.put("token" , token); return new ResponseResult (200 , dataMap); } }
使用注入的authenticationManager的authenticate方法传入一个AuthenticationManager类型
使用UsernamePasswordAuthenticationToken,传入用户名和密码即可
authenticate.getPrincipal()可以拿取UserDetails类型数据,因为之前的LoginUser实体类已经实现了UserDetails,可以直接接收,使用(LoginUser)指定类型
loginUser.getUser().getId() 拿取登录用户的id,使用jwt的工具类以用户的id生成唯一密钥为”token”,过期时间为10分钟的token
将返回的消息封装返回,使用map可以使data如{token: ‘token’}这种格式
到此用户登录请求完毕,开始做jwt过滤器 jwt过滤器主要的功能是接收前端放在请求头的token,使用token获取用户信息,存入SecurityContextHolder供后续模块调用
定义过滤器
建立filter 下 JwtAuthenticationTokenFilter文件
继承OncePerRequestFilter重写doFilterInternal方法
需要实现的操作:
获取请求过来携带的token
解析token
向redis获取用户信息
将用户信息存入SecurityContextHolder
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package com.fsan.springsecurity.filter;import com.fsan.springsecurity.Utils.JwtUtil;import com.fsan.springsecurity.Utils.RedisUtil;import com.fsan.springsecurity.pojo.LoginUser;import io.jsonwebtoken.Claims;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.stereotype.Component;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.Objects;@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisUtil redisUtil; @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader("token" ); if (!StringUtils.hasText(token)) { filterChain.doFilter(request, response); return ; } String userId; try { Claims claims = JwtUtil.parseJWT("token" , token); userId = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException ("token非法!" ); } String redisKey = "login:" + userId; LoginUser loginUser = (LoginUser) redisUtil.getCacheObject(redisKey); if (Objects.isNull(loginUser)) { throw new RuntimeException ("用户未登录!" ); } UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken (loginUser, null , null ); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request, response); } }
request.getHeader(“token”) 这里是取了请求头中token属性的值,但是没有的话就要判断
!StringUtils.hasText(token) 判断token是否存在
JwtUtil.parseJWT(“token”, token) 第一个token是我设置的加密密钥
claims.getSubject() 使用getSubject方法获取解密之后的值,也就是之前加密的userId
在security配置类中添加这个过滤器 添加一个过滤器要指定两个参数
添加的Filter类(过滤器类)
在哪个位置添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override protected void configure (HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/login" ).anonymous() .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); }
在UsernamePasswordAuthenticationFilter过滤器之前添加
这是一个请求判断的过滤器,所以要放在前面
授权实现 授权基本流程 在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
然后设置我们的资源所需要的权限即可。
配置类中配置开启权限控制方案 1 2 3 @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {
使用@PreAuthorize定义方法所需要的权限 1 2 3 4 5 6 7 8 9 @RestController public class HelloController { @RequestMapping("/hello") @PreAuthorize("hasAuthority('test')") public String hello () { return "hello" ; } }
hasAuthority 这里去调用这个方法判断是否有这个权限,现在是写死的
封装权限信息 由于LoginUser中只写了User字段,现在要存权限要加上一个List类型的字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 package com.fsan.springsecurity.pojo;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;import java.util.List;@Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails { private User user; private List<String> permissions; @Override public Collection<? extends GrantedAuthority > getAuthorities() { List<SimpleGrantedAuthority> newList = permissions.stream() .map(SimpleGrantedAuthority::new ) .collect(Collectors.toList()); return newList; } @Override public String getPassword () { return user.getPassword(); } @Override public String getUsername () { return user.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 ; } }
security在从实体类拿出权限列表的时候,使用的是getAuthorities方法,返回值为GrantedAuthority的集合类型,所以要在返回前将集合中字符串转为实现GrantedAuthority的SimpleGrantedAuthority类型
stream() 转为集合流
collect(Collectors.toList()) 固定写法,将Stream流转为List对象
map(SimpleGrantedAuthority::new) 将每一个List对象转为map中设置的返回结果
如map(Person::getName) 即是将原List中的字符串转为Person类中的getName返回值,所以结果为存储Person类型的列表对象,使用getName获取原List元素
简单理解:map中指定转化后的类型和对原List元素的放置
取名为 permissions
但是这样权限的列表每次取出都需要序列化,所以优化如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @JSONField(serialize = false) private List<SimpleGrantedAuthority> authorities;@Override public Collection<? extends GrantedAuthority > getAuthorities() { if (authorities != null ) { return authorities; } authorities = permissions.stream() .map(SimpleGrantedAuthority::new ) .collect(Collectors.toList()); return authorities; }
定义一个属性用来存转换后的权限列表,当有值时直接返回即可
但是这样在存入redis时,List<SimpleGrantedAuthority>这种类型是无法存入的,需要加上@JSONField(serialize = false)表示无需序列化
从数据库查询权限信息 创建RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用,通用的权限模型
创建以下五个数据表
用户表(user)
权限表(menu)
角色表(role)
角色权限关联表(role_menu)
用户角色关联表(user_role)
sql联表根据用户id查询权限:
1 2 3 4 5 6 7 8 9 10 11 SELECT DISTINCT m.permsFROM sys_user_role ur LEFT JOIN sys_role r ON ur.role_id = r.id LEFT JOIN sys_role_menu rm ON ur.role_id = rm.role_id LEFT JOIN sys_menu m ON m.id = rm.menu_id WHERE user_id = 1 AND r.`status` = 0 AND m.`status` = 0
distinct 排除重复项(因为一个用户可能有多个角色,每个角色的权限会有重复)
菜单表的实体类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.fsan.springsecurity.pojo;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import com.fasterxml.jackson.annotation.JsonInclude;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import java.io.Serializable;import java.util.Date;@Data @NoArgsConstructor @AllArgsConstructor @TableName(value = "sys_menu") @JsonInclude(JsonInclude.Include.NON_NULL) public class Menu implements Serializable { private static final Long serialVersionUID = -54979041104113736L ; @TableId private Long id; private String menuName; private String path; private String component; private String visible; private String status; private String icon; private Long createBy; private Date createTime; private Long updateBy; private Date updateTime; private Integer delFlag; private String remark; }
实现Serializable类表示该类可以序列化,必须要加上serialVersionUID属性
使用mybatis plus自定义配置实现多表联查
创建MenuMapper
创建配置文件
在spring配置文件中指定mapper配置的地址
创建MenuMapper:
1 2 3 4 5 6 7 8 9 10 11 12 package com.fsan.springsecurity.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.fsan.springsecurity.pojo.Menu;import org.springframework.stereotype.Repository;import java.util.List;@Repository public interface MenuMapper extends BaseMapper <Menu> { List<String> selectPermsByUserId (Long userid) ; }
创建配置文件(resources.mapper.MenuMapper.xml)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.fsan.springsecurity.mapper.MenuMapper" > <select id ="selectPermsByUserId" resultType ="java.lang.String" > SELECT DISTINCT m.perms FROM sys_user_role ur LEFT JOIN sys_role r ON ur.role_id = r.id LEFT JOIN sys_role_menu rm ON ur.role_id = rm.role_id LEFT JOIN sys_menu m ON m.id = rm.menu_id WHERE user_id = #{userid} AND r.`status` = 0 AND m.`status` = 0 </select > </mapper >
只要将之前写的多表联查语句放上修改用户id即可 #{userid}
id mapper中方法名
在spring配置文件中指定mapper配置的地址
1 2 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml
这里查看源码发现默认就是classpath*:/mapper/**/*.xml,所以创建mapper下的配置文件的时候,不定义也没事
在UserDetails中获取权限 先写测试类获取权限列表:
1 2 3 4 @Test void testSelectPermsByUserId () { System.out.println(menuMapper.selectPermsByUserId(1L )); }
使用的userid为Long类型,最后加上 L
获取数据列表没有问题之后,加入UserDetailsService
整合进UserDetailsService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(User::getUserName, username); User user = sysUserMapper.selectOne(queryWrapper); if (Objects.isNull(user)) { throw new RuntimeException ("用户不存在!" ); } List<String> authList = menuMapper.selectPermsByUserId(user.getId()); return new LoginUser (user, authList); }
之前使用的是假数据,现在改为获取权限列表之后,在controller处修改真实限制
在controller处修改真实限制:(以测试为例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.fsan.springsecurity.controller;import org.springframework.security.access.prepost.PreAuthorize;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController public class HelloController { @RequestMapping("/hello") @PreAuthorize("hasAuthority('system:test:list')") public String hello () { return "hello" ; } }
自定义权限校验方法 除了直接使用security的hasAuthority, hasAnyAuthority, 等权限校验方法,我们还可以自定义权限校验方法,实现复杂的权限校验
创建自定义权限类 1 2 3 4 5 6 7 8 9 10 11 12 @Component("hasMyAuth") public class UserPreAuth { public boolean hasAuthority (String authority) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginuser = (LoginUser) authentication.getPrincipal(); List<String> list = loginuser.getPermissions(); return list.contains(authority); } }
通过jwt过滤器存的SecurityContextHolder中拿到当前登录用户的权限列表
getPermissions方法是上面封装的没有转类型的权限数据
@Component(“hasMyAuth”) 对注入spring容器的bean取别名,方便下一步调用
测试 1 2 3 4 5 6 7 8 9 @RestController public class HelloController { @RequestMapping("/hello") @PreAuthorize("@hasMyAuth.hasAuthority('system:test:list')") public String hello () { return "hello" ; } }
在SPEL表达式中使用@ 获取容器中bean的别名对象,然后再调用这个对象类的方法
数据库联表知识 MySql LEFT JOIN(左连接) left join 子句允许您从两个或多个数据库表查询数据。left join子句是select语句的可选部分,出现在form子句之后。
假设需要从t1,t2查询数据。以下语句说明了连接两个表的left join子句的语法:
1 2 3 4 5 6 SELECT t1.c1, t1.c2, t2.c1, t2.c2FROM t1 LEFT JOIN t2 ON t1.c1 = t2.c1
连接条件则是t1.c1 = t2.c1
讲的通俗一点就是:t2表的满足t1.c1 = t2.c1的部分在查询时加入了t1表
更多用法: 1 2 3 4 5 SELECT c.customerNumber, customerName, orderNumber, status FROM customers c LEFT JOIN orders ON c.customerNumber = o.customerNumber
在以上子句中将customers数据表取别名为c,两个字段一致,则可以写作:
1 2 3 4 5 SELECT c.customerNumber, customerName, orderNumber, status FROM customers c LEFT JOIN orders USING (customerNumber)
using 使用
自定义异常处理 如果是认证过程出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法取进行异常处理。
如果是授权过程中出想的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法取进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可
实现AuthenticationEntryPoint完成认证失败的错误处理
创建handler下的AuthenticationEntryPointImpl
实现AuthenticationEntryPoint,重写commence方法
通过设置响应体来将字符串渲染到客户端
WebUtils:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.fsan.springsecurity.Utils;import javax.servlet.http.HttpServletResponse;import java.io.IOException;public class WebUtils { public static String renderString (HttpServletResponse response, String string) { try { response.setStatus(200 ); response.setContentType("application/json" ); response.setCharacterEncoding("utf-8" ); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } return null ; } }
AuthenticationEntryPointImpl:
1 2 3 4 5 6 7 8 9 10 11 @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence (HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseResult result = new ResponseResult (HttpStatus.UNAUTHORIZED.value(),"用户认证失败,请重新登录!" ); String json = JSON.toJSONString(result); WebUtils.renderString(response, json); } }
HttpStatus这个枚举类直接指定401报错,记得使用value拿到401
将ResponseResult转为json字符串然后使用WebUtils的响应体返回客户端
这里的JSON.toJSONString是fastjson
404 肯定为请求地址写错了
实现AccessDeniedHandler完成权限不足的错误处理
创建handler下的AccessDeniedHandlerImpl
实现AccessDeniedHandler,重写handle方法
设置响应体来返回字符串给客户端
1 2 3 4 5 6 7 8 9 10 11 @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle (HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ResponseResult result = new ResponseResult (HttpStatus.FORBIDDEN.value(),"您的权限不足!" ); String json = JSON.toJSONString(result); WebUtils.renderString(response, json); } }
权限不足的错误处理其实和上面的认证失败一样,只是认证失败使用的是401,权限不足返回的错误为403
在security配置类中配置异常处理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Override protected void configure (HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/login" ).anonymous() .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); http.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); }
解决security跨域 非常简单,在配置类中使用cors方法即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Override protected void configure (HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/login" ).anonymous() .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); http.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); http.cors(); }
使用权限配置权限控制 在方法上使用注解,在有很多方法需要控制的时候,查找比较麻烦,所以可以在security的配置类中配置全部方法的一个权限情况,方便管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Override protected void configure (HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/login" ).anonymous() .antMatchers("/test" ).hasAuthority("system:dept:list" ) .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); http.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); http.cors(); }
在这个方法中直接传入要控制的路径,后续调用的方法和使用注解是一样的
CSRF CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。
SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储在cookie中,并且需要前端代码去把token设置到请求头中才可以,所以csrf攻击也就不用担心了。
认证成功和失败处理器 实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器,AuthenticationFailureHandler就是登录失败
源码可以查看UsernamePasswordAuthenticationFilter的父类下的doFilter方法
但按照我们上面的jwt流程的话是完全用不到这两个处理器的,这里算是扩展
认证成功处理器 写一个类,实现AuthenticationSuccessHandler类
1 2 3 4 5 6 7 8 @Slf4j @Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { log.info("用户认证通过!" ); } }
实现AuthenticationSuccessHandler后,下面有两个方法,但是有一个defalut修饰可不重写
在配置类中添加formLogin配置
1 2 3 4 5 6 7 8 9 10 @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailureHandler); http.authorizeRequests().anyRequest().authenticated(); }
认证失败处理器 创个类,实现AuthenticationFailureHandler类
1 2 3 4 5 6 7 8 @Slf4j @Component public class LoginFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure (HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { log.info("用户认证失败!" ); } }
添加配置类(看上面的成功处理器即可)
注销成功处理器 注销成功处理器 写一个类实现LogoutSuccessHandler
1 2 3 4 5 6 7 8 @Slf4j @Component public class MyLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { log.info("用户注销成功!" ); } }
在配置类中添加相关配置
1 2 3 4 5 6 @Autowired private MyLogoutSuccessHandler myLogoutSuccessHandler; http.logout() .logoutSuccessHandler(myLogoutSuccessHandler);