SpringSecurity学习
快速入门
添加依赖
不要再因为写错单词而找一天的bug了,spring-boot-starter-security中的security,不是securtiy,以后要下载依赖尽量不要自己手写,记得去maven仓库copy
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.0</version></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency></dependencies>
创建启动类
package gdut.edu;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class MyApplication {public static void main(String[] args) {SpringApplication.run(MyApplication.class, args);}}
创建controller
package gdut.edu.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class HelloController {@GetMapping("/hello")public String hello() {return "hello";}}
认证
登录校验流程
前端发请求登录,后端查询数据库对比用户账号密码是否正确,正确则加密用户信息,返回jwt给前端,前端将token信息保存到本地,之后每次发送请求都携带token,后端对token解密,查看用户是否有权限访问,授予用户访问权限。
认证流程

思路分析
登录
- 自定义登录接口
- 调用ProviderManager的方法进行验证,验证通过生成jwt将用户信息保存到redis
- 自定义UserDetailsService
- 在这个实现类中去查询数据库
校验
- 定义jwt认证过滤器
- 获取token,解析token后获取其中的userId,从redis中获取用户信息,存入SecurityContextHolder
准备工作
- 添加依赖
<!--redis依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--fastjson依赖--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.33</version></dependency><!--jwt依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency>
- 添加redis相关配置
import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.serializer.SerializerFeature;import com.fasterxml.jackson.databind.JavaType;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.type.TypeFactory;import org.springframework.data.redis.serializer.RedisSerializer;import org.springframework.data.redis.serializer.SerializationException;import com.alibaba.fastjson.parser.ParserConfig;import org.springframework.util.Assert;import java.nio.charset.Charset;/*** Redis使用FastJson序列化*/public class FastJsonRedisSerializer<T> implements RedisSerializer<T>{public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;static{ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJsonRedisSerializer(Class<T> clazz){super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException{if (t == null){return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException{if (bytes == null || bytes.length <= 0){return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}protected JavaType getJavaType(Class<?> clazz){return TypeFactory.defaultInstance().constructType(clazz);}}
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.StringRedisSerializer;@Configurationpublic class RedisConfig {@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}}
- 响应类
import com.fasterxml.jackson.annotation.JsonInclude;@JsonInclude(JsonInclude.Include.NON_NULL)public class ResponseResult<T> {/*** 状态码*/private Integer code;/*** 提示信息,如果有错误时,前端可以获取该字段进行提示*/private String msg;/*** 查询到的结果数据,*/private T data;public ResponseResult(Integer code, String msg) {this.code = code;this.msg = msg;}public ResponseResult(Integer code, T data) {this.code = code;this.data = data;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public T getData() {return data;}public void setData(T data) {this.data = data;}public ResponseResult(Integer code, String msg, T data) {this.code = code;this.msg = msg;this.data = data;}}
- 工具类
import io.jsonwebtoken.Claims;import io.jsonwebtoken.JwtBuilder;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import javax.crypto.SecretKey;import javax.crypto.spec.SecretKeySpec;import java.util.Base64;import java.util.Date;import java.util.UUID;/*** JWT工具类*/public class JwtUtil {//有效期为public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时//设置秘钥明文public static final String JWT_KEY = "sangeng";public static String getUUID(){String token = UUID.randomUUID().toString().replaceAll("-", "");return token;}/*** 生成jtw* @param subject token中要存放的数据(json格式)* @return*/public static String createJWT(String subject) {JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间return builder.compact();}/*** 生成jtw* @param subject token中要存放的数据(json格式)* @param ttlMillis token超时时间* @return*/public static String createJWT(String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间return builder.compact();}private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;SecretKey secretKey = generalKey();long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);if(ttlMillis==null){ttlMillis=JwtUtil.JWT_TTL;}long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);return Jwts.builder().setId(uuid) //唯一的ID.setSubject(subject) // 主题 可以是JSON数据.setIssuer("sg") // 签发者.setIssuedAt(now) // 签发时间.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥.setExpiration(expDate);}/*** 创建token* @param id* @param subject* @param ttlMillis* @return*/public static String createJWT(String id, String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间return builder.compact();}public static void main(String[] args) throws Exception {String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";Claims claims = parseJWT(token);System.out.println(claims);}/*** 生成加密后的秘钥 secretKey* @return*/public static SecretKey generalKey() {byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}/*** 解析** @param jwt* @return* @throws Exception*/public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey = generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}}
import java.util.*;import java.util.concurrent.TimeUnit;@SuppressWarnings(value = { "unchecked", "rawtypes" })@Componentpublic class RedisCache{@Autowiredpublic RedisTemplate redisTemplate;/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String key, final T value){redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值* @param timeout 时间* @param timeUnit 时间颗粒度*/public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit){redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 设置有效时间** @param key Redis键* @param timeout 超时时间* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout){return expire(key, timeout, TimeUnit.SECONDS);}/*** 设置有效时间** @param key Redis键* @param timeout 超时时间* @param unit 时间单位* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout, final TimeUnit unit){return redisTemplate.expire(key, timeout, unit);}/*** 获得缓存的基本对象。** @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String key){ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}/*** 删除单个对象** @param key*/public boolean deleteObject(final String key){return redisTemplate.delete(key);}/*** 删除集合对象** @param collection 多个对象* @return*/public long deleteObject(final Collection collection){return redisTemplate.delete(collection);}/*** 缓存List数据** @param key 缓存的键值* @param dataList 待缓存的List数据* @return 缓存的对象*/public <T> long setCacheList(final String key, final List<T> dataList){Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 获得缓存的list对象** @param key 缓存的键值* @return 缓存键值对应的数据*/public <T> List<T> getCacheList(final String key){return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set** @param key 缓存键值* @param dataSet 缓存的数据* @return 缓存数据的对象*/public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet){BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()){setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set** @param key* @return*/public <T> Set<T> getCacheSet(final String key){return redisTemplate.opsForSet().members(key);}/*** 缓存Map** @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap){if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map** @param key* @return*/public <T> Map<String, T> getCacheMap(final String key){return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入数据** @param key Redis键* @param hKey Hash键* @param value 值*/public <T> void setCacheMapValue(final String key, final String hKey, final T value){redisTemplate.opsForHash().put(key, hKey, value);}/*** 获取Hash中的数据** @param key Redis键* @param hKey Hash键* @return Hash中的对象*/public <T> T getCacheMapValue(final String key, final String hKey){HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 删除Hash中的数据** @param key* @param hkey*/public void delCacheMapValue(final String key, final String hkey){HashOperations hashOperations = redisTemplate.opsForHash();hashOperations.delete(key, hkey);}/*** 获取多个Hash中的数据** @param key Redis键* @param hKeys Hash键集合* @return Hash对象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys){return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 获得缓存的基本对象列表** @param pattern 字符串前缀* @return 对象列表*/public Collection<String> keys(final String pattern){return redisTemplate.keys(pattern);}}
import javax.servlet.http.HttpServletResponse;import java.io.IOException;public class WebUtils{/*** 将字符串渲染到客户端** @param response 渲染对象* @param string 待渲染的字符串* @return null*/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;}}
- 实体类
import java.io.Serializable;import java.util.Date;/*** 用户表(User)实体类*/@Data@AllArgsConstructor@NoArgsConstructorpublic class User implements Serializable {private static final long serialVersionUID = -40356785423868312L;/*** 主键*/private Long id;/*** 用户名*/private String userName;/*** 昵称*/private String nickName;/*** 密码*/private String password;/*** 账号状态(0正常 1停用)*/private String status;/*** 邮箱*/private String email;/*** 手机号*/private String phonenumber;/*** 用户性别(0男,1女,2未知)*/private String sex;/*** 头像*/private String avatar;/*** 用户类型(0管理员,1普通用户)*/private String userType;/*** 创建人的用户id*/private Long createBy;/*** 创建时间*/private Date createTime;/*** 更新人*/private Long updateBy;/*** 更新时间*/private Date updateTime;/*** 删除标志(0代表未删除,1代表已删除)*/private Integer delFlag;}
实现
数据库校验用户
自定义UserDetailsService,让SpringSecurity使用我们的UserDetailsService,使用自定义的UserDetailsService可以从数据库查询用户名和密码
准备工作
- 新建用户表
CREATE TABLE `sys_user` (`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',`user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',`nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',`password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',`status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',`email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱',`phonenumber` VARCHAR(32) DEFAULT NULL COMMENT '手机号',`sex` CHAR(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',`avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像',`user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',`create_by` BIGINT(20) DEFAULT NULL COMMENT '创建人的用户id',`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',`update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',`del_flag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',PRIMARY KEY (`id`)) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
- 引入MybatisPlus和Mysql驱动依赖
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency>
- 配置数据库连接
spring:datasource:url: jdbc:mysql://localhost:3306/springsecurity?characterEncoding=utf-8&serverTimezone=UTCusername: rootpassword: 5247driver-class-name: com.mysql.cj.jdbc.Driver
- 修改实体类User
- 类名上加@TableName(value = “sys_user”) ,id字段上加 @TableId(type = IdType.AUTO)
- 定义UserMapper接口
public interface UserMapper extends BaseMapper<User> {}
- 配置Mapper扫描
package gdut.edu;import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication@MapperScan("gdut.edu.Mapper")public class MyApplication {public static void main(String[] args) {SpringApplication.run(MyApplication.class,args);}}
- 添加junit测试依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency>
- 测试MP是否可以正常使用
@SpringBootTestpublic class UserMapperTest {@Autowiredprivate UserMapper userMapper;@Testpublic void testInsert(){User user = new User();user.setNickName("name1");user.setSex("1");user.setUserName("张三");int res = userMapper.insert(user);System.out.println(res);}}
核心代码实现
- 创建一个UserDetailsServiceImpl实现UserDetailsService接口,并重写其中的方法,根据用户名从数据库查询用户信息
@Servicepublic class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {// 根据用户名查询用户信息LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUserName,"dt");User user = userMapper.selectOne(queryWrapper);if(Objects.isNull(user)){throw new RuntimeException("用户名或密码错误");}// TODO 根据用户查询权限信息,添加到LoginUser中// 封装成UserDetails对象返回return new UserLogin(user);}}
- 因为UserDetailsService返回值是UserDetails类型,所以定义一个UserLogin类,实现该接口,把用户信息封装在里面
@Data@NoArgsConstructor@AllArgsConstructorpublic class UserLogin implements UserDetails {private User user;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}}
- 注意:如果要测试,需要插入数据到数据库,如果想要明文存储密码,需要在密码的前面加上{noop},例如

- 这样登录的时候就可以用dt作为用户名,1234作为密码了
密码加密存储
实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
@Configurationpublic class SecurityConfig {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}}
- 测试
@Autowiredprivate PasswordEncoder passwordEncoder;@Testpublic void encodeTest() {String password = "1234";password = passwordEncoder.encode(password);System.out.println(password);System.out.println(passwordEncoder.matches("1234", password));}
Jwt测试
@Testpublic void jwtTest() throws Exception {String jwt = JwtUtil.createJWT("1234");System.out.println(jwt);Claims claims = JwtUtil.parseJWT(jwt);System.out.println(claims.getSubject());}
登录接口
接下我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。
- 登录接口
@RestControllerpublic class LoginController {@Autowiredprivate LoginService loginService;@PostMapping("/user/login")public ResponseResult login(@RequestBody User user) {System.out.println(user);return loginService.login(user);}}
- SecurityConfig配置,注入AuthenticationManager到容器,放行/user/login接口
@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}}
- 认证成功生成jwt,并放入响应中返回给前端保存到本地,将用户信息保存到redis
@Servicepublic class LoginServiceImpl extends ServiceImpl<UserMapper, User> implements LoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;public ResponseResult login(User user) {// AuthenticationManager authenticate 进行用户认证UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);// 如果认证没有通过,给出用户提示if (Objects.isNull(authenticate)){throw new RuntimeException("登录失败");}// 认证通过,使用userid生成一个jwt,jwt存入ResponseResult返回UserLogin userLogin = (UserLogin) authenticate.getPrincipal();String userId = userLogin.getUser().getId().toString();String jwt = JwtUtil.createJWT(userId);// 把完整的用户信息存入redis,userid为keyredisCache.setCacheObject("login:"+userId,userLogin);HashMap<String, String> map = new HashMap<>();map.put("token",jwt);return new ResponseResult(200,"登录成功",map);}}
认证过滤器
我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。使用userid去redis中获取对应的UserLogin对象。然后封装Authentication对象存入SecurityContextHolder。
- 自定义JwtAuthenticationTokenFilter过滤器(注意:继承的是OncePerRequestFilter)
/*** @Description token认证的过滤器* @Author JiaFu* @Date 2022/7/5 15:49*/@Componentpublic class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 获取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {// 放行filterChain.doFilter(request,response);// 这里需要return防止走完后面的过滤器之后又接着执行下面解析token的代码return;}// 解析tokenString userId;try {Claims claims = JwtUtil.parseJWT(token);userId = claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("token非法");}// 从redis中获取用户信息String redisKey = "login:" + userId;UserLogin userLogin = redisCache.getCacheObject(redisKey);if (Objects.isNull(userLogin)){throw new RuntimeException("用户未登录");}// 将用户信息存入SecurityContextHolderUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userLogin, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);// 放行filterChain.doFilter(request,response);}}
- 将自定义的过滤器添加到过滤器链当中,添加到UsernamePasswordAuthenticationFilter的前面
@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();// 把token校验过滤器添加到过滤器链当中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}}
推出登录
我们只需要定义一个推出登录接口/user/logout,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。
- 接口
@RestControllerpublic class LoginController {@Autowiredprivate LoginService loginService;@PostMapping("/user/login")public ResponseResult login(@RequestBody User user) {System.out.println(user);return loginService.login(user);}@GetMapping("/user/logout")public ResponseResult logout(){return loginService.logout();}}
- 推出登录的实现
@Servicepublic class LoginServiceImpl extends ServiceImpl<UserMapper, User> implements LoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;public ResponseResult login(User user) {// AuthenticationManager authenticate 进行用户认证UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);// 如果认证没有通过,给出用户提示if (Objects.isNull(authenticate)){throw new RuntimeException("登录失败");}// 认证通过,使用userid生成一个jwt,jwt存入ResponseResult返回UserLogin userLogin = (UserLogin) authenticate.getPrincipal();String userId = userLogin.getUser().getId().toString();String jwt = JwtUtil.createJWT(userId);// 把完整的用户信息存入redis,userid为keyredisCache.setCacheObject("login:"+userId,userLogin);HashMap<String, String> map = new HashMap<>();map.put("token",jwt);return new ResponseResult(200,"登录成功",map);}@Overridepublic ResponseResult logout() {// 获取SecurityContextHolder中的userIdAuthentication authentication = SecurityContextHolder.getContext().getAuthentication();UserLogin userLogin = (UserLogin) authentication.getPrincipal();Long userId = userLogin.getUser().getId();// 删除redis中user的信息redisCache.deleteObject("login:"+userId);return new ResponseResult(200,"注销成功");}}
SpringSecurity的一些配置(api的访问)
@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问,注意:只有在没有登录的情况下可以访问,已登录了则不可以访问.antMatchers("/user/login").anonymous().antMatchers("/hello").permitAll()//允许用户登录或不登录都可以访问// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();// 把token校验过滤器添加到过滤器链当中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}
授权
授权的作用
例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。
总结起来就是**不同的用户可以使用不同的功能**。这就是权限系统要去实现的效果。我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。
授权的基本流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。然后设置我们的资源所需要的权限即可。
授权的实现
限制访问资源所需权限
SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。
但是要使用它我们需要先开启相关配置。`@EnableGlobalMethodSecurity(prePostEnabled = true)`
@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问,注意:只有在没有登录的情况下可以访问,已登录了则不可以访问.antMatchers("/user/login").anonymous().antMatchers("/hello").permitAll()//允许用户登录或不登录都可以访问// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();// 把token校验过滤器添加到过滤器链当中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}}
然后就可以使用对应的注解。@PreAuthorize
@RestControllerpublic class HelloController {@GetMapping("/hello")@PreAuthorize("hasAuthority('test')")public String hello(){return "hello";}}
封装权限信息
我们前面在写UserDetailsServiceImpl的时候说过,在查询出用户后还要获取对应的权限信息,封装到UserDetails中返回。
我们先直接把权限信息写死封装到UserDetails中进行测试。我们之前定义了UserDetails的实现类LoginUser,想要让其能封装权限信息就要对其进行修改。
@Data@NoArgsConstructorpublic class UserLogin implements UserDetails {private User user;// 存储权限信息private List<String> permissions;// 存储SpringSecurity所需的权限信息的集合@JSONField(serialize = false)private List<GrantedAuthority> authorities;public UserLogin(User user, List<String> permissions) {this.user = user;this.permissions = permissions;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {if (authorities != null){return authorities;}// for (String permission : permissions) {// SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);// authorities.add(simpleGrantedAuthority);// }//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());return authorities;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}}
LoginUser修改完后我们就可以在UserDetailsServiceImpl中去把权限信息封装到LoginUser中了。我们写死权限进行测试,后面我们再从数据库中查询权限信息。
@Servicepublic class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {// 根据用户名查询用户信息LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUserName,"dt");User user = userMapper.selectOne(queryWrapper);if(Objects.isNull(user)){throw new RuntimeException("用户名或密码错误");}// TODO 根据用户查询权限信息,添加到LoginUser中ArrayList<String> list = new ArrayList<>(Arrays.asList("test","admin"));// 封装成UserDetails对象返回return new UserLogin(user,list);}}
自定义失败处理
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。如果是认证过程中出现的异常会被封装成AuthenticationException然后调用**AuthenticationEntryPoint**对象的方法去进行异常处理。如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用**AccessDeniedHandler**对象的方法去进行异常处理。所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
- 自定义实现类
/*** @Description 权限异常处理器* @Author JiaFu* @Date 2022/7/5 21:07*/@Componentpublic class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {ResponseResult<Object> responseResult = new ResponseResult<>(HttpStatus.UNAUTHORIZED.value(), "权限不足");String json = JSON.toJSONString(responseResult);WebUtils.renderString(response,json);}}
/*** @Description 认证异常处理器* @Author JiaFu* @Date 2022/7/5 21:12*/@Componentpublic class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {ResponseResult<Object> responseResult = new ResponseResult<>(HttpStatus.FORBIDDEN.value(), "认证失败,请重新登录");String json = JSON.toJSONString(responseResult);WebUtils.renderString(response,json);}}
- 配置给SpringSecurity
注入对应的处理器
@Autowiredprivate AccessDeniedHandler accessDeniedHandler;@Autowiredprivate AuthenticationEntryPoint authenticationEntryPoint;
然后使用HttpSecurity对象的方法去配置
// 配置异常处理器http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(authenticationEntryPoint);
跨域
浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。
前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。所以我们就要处理一下,让前端能进行跨域请求。(解决跨域问题的实质是给响应头设置一些字段及值)
- 先对SpringBoot配置,运行跨域请求
/*** @Description 跨域配置* @Author JiaFu* @Date 2022/7/5 21:45*/@Configurationpublic class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping("/**")// 设置允许跨域请求的域名.allowedOriginPatterns("*")// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods("GET", "POST", "DELETE", "PUT")// 设置允许的header的属性.allowedHeaders("*")// 设置允许跨域的时间.maxAge(3600);}}
- 开启SpringSecurity的跨域访问,由于我们的资源都会收到SpringSecurity的保护,所以想要跨域访问还要让SpringSecurity运行跨域访问。配置securityconfig
@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问,注意:只有在没有登录的情况下可以访问,已登录了则不可以访问.antMatchers("/user/login").anonymous()// .antMatchers("/hello").permitAll()//允许用户登录或不登录都可以访问// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();// 把token校验过滤器添加到过滤器链当中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// 配置异常处理器http/*** @Description 自定义权限* @Author JiaFu* @Date 2022/7/6 9:41*/@Component("ex")public class JFExpressionRoot {public boolean hasAuthority(String authority){// 获取当前用户权限Authentication authentication = SecurityContextHolder.getContext().getAuthentication();UserLogin userLogin = (UserLogin) authentication.getPrincipal();List<String> permissions = userLogin.getPermissions();// 判断权限集合中是否存在authorityreturn permissions.contains(authority);}}.exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(authenticationEntryPoint);// 允许跨域http.cors();}
自定义权限校验
我们也可以定义自己的权限校验方法,在@PreAuthorize注解中使用我们的方法。
/*** @Description 自定义权限* @Author JiaFu* @Date 2022/7/6 9:41*/@Component("ex")public class JFExpressionRoot {public boolean hasAuthority(String authority){// 获取当前用户权限Authentication authentication = SecurityContextHolder.getContext().getAuthentication();UserLogin userLogin = (UserLogin) authentication.getPrincipal();List<String> permissions = userLogin.getPermissions();// 判断权限集合中是否存在authorityreturn permissions.contains(authority);}}
在SPEL表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的hasAuthority方法
@GetMapping("/hello")// @PreAuthorize("hasAuthority('test')")@PreAuthorize("@ex.hasAuthority('test')")public String hello(){return "hello";}
基于配置的权限控制
http.antMatchers("/hello").hasAuthority("test")
@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问,注意:只有在没有登录的情况下可以访问,已登录了则不可以访问.antMatchers("/user/login").anonymous()// .antMatchers("/hello").permitAll()//允许用户登录或不登录都可以访问.antMatchers("/hello").hasAuthority("test")//用户需要有test权限才可以访问// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();// 把token校验过滤器添加到过滤器链当中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// 配置异常处理器http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(authenticationEntryPoint);// 允许跨域http.cors();}
