一、JWT简介
JWT 全称 JSON Web Token,JWT 主要用于用户登录鉴权,当用户登录之后,返回给前端一个Token,之后用户利用Token进行信息交互。
除了JWT认证之外,比较传统的还有Session认证,如何选择可以查看之前的博文:基于token与Session身份认证对比-CSDN博客简单来说认证就是让服务器知道你是谁?就是让服务器知道你能干什么,不能干什么?那么基于身份认证我们一般有俩种方式:Session-Cookie和JWT。https://blog.csdn.net/shaogaiyue9745602/article/details/135130114?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22135130114%22%2C%22source%22%3A%22shaogaiyue9745602%22%7D
二、Spring Security简介
Spring Security 是基于 Spring 的身份认证(Authentication)和用户授权(Authorization)框架,提供了一套 Web 应用安全性的完整解决方案。其中核心技术使用了 Servlet 过滤器、IOC 和 AOP 等。实际操作时经常需要实现XXXFilter来自定义的登录以及访问控制。
- 什么是身份认证
身份认证指的是用户去访问系统资源时,系统要求验证用户的身份信息,用户身份合法才访问对应资源。常见的身份认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
- 什么是用户授权
当身份认证通过后,去访问系统的资源,系统会判断用户是否拥有访问该资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。比如 会员管理模块有增删改查功能,有的用户只能进行查询,而有的用户可以进行修改、删除。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
三、登录拦截流程
其中认证的过程还应包含 鉴权 以及 token过期 验证。
四、代码实现
代码实现前需要有springboot项目有基础的用户user表,并可以实现数据的查询功能。最好有自己的统一返回配置和全局异常处理配置,若没有可参考前面的博客参考进行配置或自行配置。
1.在pom中添加相关依赖
<!--安全框架引入, 进行权限控制--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.50</version></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version></dependency>
引入依赖重新启动后,访问项目发现会出现登录页面如下:
用户名默认为:user
默认密码在项目启动时会打印在控制台如下:
也可通过在配置文件 application.properties 自定义用户名和密码
spring.security.user.name=admin
spring.security.user.password=admin
2.创建SecurityUser实现UserDetails
package com.hng.config.jwtSecurity;import com.alibaba.fastjson.annotation.JSONField;
import com.hng.entity.SysUser;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;
import java.util.List;/*** @Author: 郝南过* @Description: * @Date: 2023/11/29 10:26* @Version: 1.0*/
@Data
@NoArgsConstructor
public class SecurityUser implements UserDetails {private SysUser user;private List<String> permissions;@JSONField(serialize = false) // 防止存入redis时序列化出错,不进行序列化,也不存入redis中private List<SimpleGrantedAuthority> authorities;public SecurityUser(SysUser user) {this.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;}
}
3.创建UserDetailsServiceImpl实现UserDetailsService
该类主要功能是将Security拦截的用户名密码改为查询数据库中的用户名密码,以及权限的相关认证(权限认证相关本篇暂时不做添加)
package com.hng.config.jwtSecurity;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hng.entity.SysUser;
import com.hng.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
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 javax.annotation.Resource;
import java.util.Objects;/*** @Author: 郝南过* @Description: 将Security拦截的用户名密码改为数据库中已有的用户名密码,Security自己校验密码,默认使用PasswordEncoder,格式为{id}password(id代表加密方式),* 一般不采用此方式,SpringSecurity提供了BcryptPasswordEncoder,只需将此注入到spring容器中。SpringSecurity就会使用它进行替换校验* @Date: 2023/12/11 11:29* @Version: 1.0*/
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {@Resourceprivate SysUserService sysUserService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//查询用户信息LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper();queryWrapper.eq(SysUser::getUserName,username);SysUser user = sysUserService.getOne(queryWrapper);if(Objects.isNull(user)){throw new RuntimeException("用户名或密码错误");}//TODO 查询授权信息return new SecurityUser(user);}
}
4.创建JwtTokenUtil工具类
该工具类用于token的创建验证等
package com.hng.config.jwtSecurity;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;/*** @Description JwtToken生成的工具类* @Version 1.0**/
@Slf4j
@Component
public class JwtTokenUtil {private static final String CLAIM_KEY_USERNAME = "username";private static final String CLAIM_KEY_CREATED = "created";private static final String CLAIM_KEY_USER_ID = "userId";// 令牌自定义标识@Value("${jwt.token.header}")private String header;// 令牌秘钥@Value("${jwt.token.secret}")private String secret;// 令牌有效期(默认30分钟),也可将token的过期时间交给redis管理@Value("${jwt.token.expireTime}")private Long expiration;/*** 根据负责生成JWT的token*/private String generateToken(Map<String, Object> claims) {return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate()).signWith(SignatureAlgorithm.HS512, secret).compact();}/*** 从token中获取JWT中的负载*/public Claims getClaimsFromToken(String token) {Claims claims = null;try {claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();} catch (Exception e) {log.info("JWT格式验证失败:{}",token);}return claims;}/*** 生成token的过期时间,返回Date*/private Date generateExpirationDate() {return new Date(System.currentTimeMillis() + expiration * 1000);}/*** 从token中获取登录用户名*/public String getUserNameFromToken(String token) {String username;try {Claims claims = getClaimsFromToken(token);username = claims.getSubject();} catch (Exception e) {username = null;}return username;}/*** 验证token是否还有效** @param token 客户端传入的token* @param userDetails 从数据库中查询出来的用户信息*/public boolean validateToken(String token, UserDetails userDetails) {String username = getUserNameFromToken(token);return username.equals(userDetails.getUsername()) && !isTokenExpired(token);}/*** 判断token是否已经失效*/public boolean isTokenExpired(String token) {Date expiredDate = getExpiredDateFromToken(token);return expiredDate.before(new Date());}/*** 从token中获取过期时间*/private Date getExpiredDateFromToken(String token) {Claims claims = getClaimsFromToken(token);return claims.getExpiration();}/*** 根据用户信息生成token*/public String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());claims.put(CLAIM_KEY_CREATED, new Date());return generateToken(claims);}/*** 根据用户信息生成token*/public String generateToken(SecurityUser securityUser) {Map<String, Object> claims = new HashMap<>();claims.put(CLAIM_KEY_USERNAME, securityUser.getUsername());claims.put(CLAIM_KEY_USER_ID, securityUser.getUser().getId());claims.put(CLAIM_KEY_CREATED, new Date());return generateToken(claims);}/*** 判断token是否可以被刷新*/public boolean canRefresh(String token) {return !isTokenExpired(token);}/*** 刷新token*/public String refreshToken(String token) {Claims claims = getClaimsFromToken(token);claims.put(CLAIM_KEY_CREATED, new Date());return generateToken(claims);}/*** 获取请求token** @param request* @return token*/public String getToken(HttpServletRequest request){return request.getHeader(header);}
}
5.创建RedisUtil工具类
springboot如何集成redis可查看之前的博文
Springboot 集成Redis-CSDN博客文章浏览阅读621次,点赞9次,收藏9次。注意commons-pool2包与spring的版本一致性,若出错尝试升级或降级commons-pool2版本。https://blog.csdn.net/shaogaiyue9745602/article/details/134669420?spm=1001.2014.3001.5501
记得在配置文件application.properties中添加redis配置信息
spring.redis.host=127.0.0.1
spring.redis.host.port=6379
#spring.redis.host.name=
#spring.redis.host.password=
RedisUtil.java
package com.hng.config.redis;import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;@Component
@Order(-1)
public final class RedisUtil {@Resourceprivate RedisTemplate redisTemplate;/*** 指定缓存失效时间** @param key 键* @param time 时间(秒)* @return 0*/public boolean expire(String key, long time) {try {if (time > 0) {redisTemplate.expire(key, time, TimeUnit.SECONDS);}return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 根据key 获取过期时间** @param key 键 不能为null* @return 时间(秒) 返回0代表为永久有效*/public long getExpire(String key) {return redisTemplate.getExpire(key, TimeUnit.SECONDS);}/*** 判断key是否存在** @param key 键* @return true 存在 false不存在*/public boolean hasKey(String key) {try {return redisTemplate.hasKey(key);} catch (Exception e) {e.printStackTrace();return false;}}/*** 删除缓存** @param key 可以传一个值 或多个*/@SuppressWarnings("unchecked")public void del(String... key) {if (key != null && key.length > 0) {if (key.length == 1) {redisTemplate.delete(key[0]);} else {redisTemplate.delete(CollectionUtils.arrayToList(key));}}}// ============================String=============================/*** 普通缓存获取** @param key 键* @return 值*/public Object get(String key) {return key == null ? null : redisTemplate.opsForValue().get(key);}/*** 普通缓存放入** @param key 键* @param value 值* @return true成功 false失败*/public boolean set(String key, Object value) {try {redisTemplate.opsForValue().set(key, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 普通缓存放入并设置时间** @param key 键* @param value 值* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期* @return true成功 false 失败*/public boolean set(String key, Object value, long time) {try {if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);} else {set(key, value);}return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 递增** @param key 键* @param delta 要增加几(大于0)* @return*/public long incr(String key, long delta) {if (delta < 0) {throw new RuntimeException("递增因子必须大于0");}return redisTemplate.opsForValue().increment(key, delta);}/*** 递减** @param key 键* @param delta 要减少几(小于0)* @return*/public long decr(String key, long delta) {if (delta < 0) {throw new RuntimeException("递减因子必须大于0");}return redisTemplate.opsForValue().increment(key, -delta);}// ================================Map=================================/*** HashGet** @param key 键 不能为null* @param item 项 不能为null* @return 值*/public Object hget(String key, String item) {return redisTemplate.opsForHash().get(key, item);}/*** 获取hashKey对应的所有键值** @param key 键* @return 对应的多个键值*/public Map<Object, Object> hmget(String key) {return redisTemplate.opsForHash().entries(key);}/*** HashSet** @param key 键* @param map 对应多个键值* @return true 成功 false 失败*/public boolean hmset(String key, Map<String, Object> map) {try {redisTemplate.opsForHash().putAll(key, map);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** HashSet 并设置时间** @param key 键* @param map 对应多个键值* @param time 时间(秒)* @return true成功 false失败*/public boolean hmset(String key, Map<String, Object> map, long time) {try {redisTemplate.opsForHash().putAll(key, map);if (time > 0) {expire(key, time);}return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 向一张hash表中放入数据,如果不存在将创建** @param key 键* @param item 项* @param value 值* @return true 成功 false失败*/public boolean hset(String key, String item, Object value) {try {redisTemplate.opsForHash().put(key, item, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 向一张hash表中放入数据,如果不存在将创建* 0** @param key 键* @param item 项* @param value 值* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间* @return true 成功 false失败*/public boolean hset(String key, String item, Object value, long time) {try {redisTemplate.opsForHash().put(key, item, value);if (time > 0) {expire(key, time);}return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 删除hash表中的值** @param key 键 不能为null* @param item 项 可以使多个 不能为null*/public void hdel(String key, Object... item) {redisTemplate.opsForHash().delete(key, item);}/*** 判断hash表中是否有该项的值** @param key 键 不能为null* @param item 项 不能为null* @return true 存在 false不存在*/public boolean hHasKey(String key, String item) {return redisTemplate.opsForHash().hasKey(key, item);}/*** hash递增 如果不存在,就会创建一个 并把新增后的值返回** @param key 键* @param item 项* @param by 要增加几(大于0)* @return*/public double hincr(String key, String item, double by) {return redisTemplate.opsForHash().increment(key, item, by);}/*** hash递减** @param key 键* @param item 项* @param by 要减少记(小于0)* @return*/public double hdecr(String key, String item, double by) {return redisTemplate.opsForHash().increment(key, item, -by);}// ============================set=============================/*** 根据key获取Set中的所有值** @param key 键* @return*/public Set<Object> sGet(String key) {try {return redisTemplate.opsForSet().members(key);} catch (Exception e) {e.printStackTrace();return null;}}/*** 根据value从一个set中查询,是否存在** @param key 键* @param value 值* @return true 存在 false不存在*/public boolean sHasKey(String key, Object value) {try {return redisTemplate.opsForSet().isMember(key, value);} catch (Exception e) {e.printStackTrace();return false;}}/*** 将数据放入set缓存** @param key 键* @param values 值 可以是多个* @return 成功个数*/public long sSet(String key, Object... values) {try {return redisTemplate.opsForSet().add(key, values);} catch (Exception e) {e.printStackTrace();return 0;}}/*** 将set数据放入缓存** @param key 键* @param time 时间(秒)* @param values 值 可以是多个* @return 成功个数*/public long sSetAndTime(String key, long time, Object... values) {try {Long count = redisTemplate.opsForSet().add(key, values);if (time > 0)expire(key, time);return count;} catch (Exception e) {e.printStackTrace();return 0;}}/*** 获取set缓存的长度** @param key 键* @return*/public long sGetSetSize(String key) {try {return redisTemplate.opsForSet().size(key);} catch (Exception e) {e.printStackTrace();return 0;}}/*** 移除值为value的** @param key 键* @param values 值 可以是多个* @return 移除的个数*/public long setRemove(String key, Object... values) {try {Long count = redisTemplate.opsForSet().remove(key, values);return count;} catch (Exception e) {e.printStackTrace();return 0;}}// ===============================list=================================/*** 获取list缓存的内容** @param key 键* @param start 开始* @param end 结束 0 到 -代表所有值* @return*/public List<Object> lGet(String key, long start, long end) {try {return redisTemplate.opsForList().range(key, start, end);} catch (Exception e) {e.printStackTrace();return null;}}/*** 获取list缓存的长度** @param key 键* @return 0*/public long lGetListSize(String key) {try {return redisTemplate.opsForList().size(key);} catch (Exception e) {e.printStackTrace();return 0;}}/*** 通过索引 获取list中的值** @param key 键* @param index 索引 index>=0时, 0 表头, 第二个元素,依次类推;index<0时,-,表尾,-倒数第二个元素,依次类推* @return 0*/public Object lGetIndex(String key, long index) {try {return redisTemplate.opsForList().index(key, index);} catch (Exception e) {e.printStackTrace();return null;}}/*** 将list放入缓存** @param key 键* @param value 值* @return*/public boolean lSet(String key, Object value) {try {redisTemplate.opsForList().rightPush(key, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 将list放入缓存** @param key 键* @param value 值* @param time 时间(秒)* @return*/public boolean lSet(String key, Object value, long time) {try {redisTemplate.opsForList().rightPush(key, value);if (time > 0)expire(key, time);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 将list放入缓存** @param key 键* @param value 值* @return*/public boolean lSet(String key, List<Object> value) {try {redisTemplate.opsForList().rightPushAll(key, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 将list放入缓存** @param key 键* @param value 值* @param time 时间(秒)* @return 0*/public boolean lSet(String key, List<Object> value, long time) {try {redisTemplate.opsForList().rightPushAll(key, value);if (time > 0)expire(key, time);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 根据索引修改list中的某条数据** @param key 键* @param index 索引* @param value 值* @return 0*/public boolean lUpdateIndex(String key, long index, Object value) {try {redisTemplate.opsForList().set(key, index, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 移除N个值为value** @param key 键* @param count 移除多少个* @param value 值* @return 移除的个数*/public long lRemove(String key, long count, Object value) {try {Long remove = redisTemplate.opsForList().remove(key, count, value);return remove;} catch (Exception e) {e.printStackTrace();return 0;}}
}
6. 创建JwtAuthenticationTokenFilter实现OncePerRequestFilter
该类为登录授权过滤器
package com.hng.config.jwtSecurity;import com.hng.config.redis.RedisUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Qualifier;
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 org.springframework.web.servlet.HandlerExceptionResolver;import javax.annotation.Resource;
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;/*** @Author: 郝南过* @Description: JWT 登录授权过滤器* @Date: 2023/11/28 16:19* @Version: 1.0*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Resourceprivate RedisUtil redisUtil;// JWT 工具类@Resourceprivate JwtTokenUtil jwtTokenUtil;@Resource@Qualifier("handlerExceptionResolver")private HandlerExceptionResolver resolver;/*** 从请求中获取 JWT 令牌,并根据令牌获取用户信息,最后将用户信息封装到 Authentication 中,* 方便后续校验(只会执行一次)* @param request* @param response* @param filterChain* @throws ServletException* @throws IOException*/@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {//token为空的话, 就不管它, 让SpringSecurity中的其他过滤器处理请求,请求放行filterChain.doFilter(request, response);return;}//token不为空时, 解析tokenString userId = null;try {Claims claims = jwtTokenUtil.getClaimsFromToken(token);//解析出userIduserId = claims.get("userId", String.class);} catch (Exception e) {e.printStackTrace();// 交给全局异常处理类处理throw new RuntimeException("token非法");
// resolver.resolveException(request, response, null,new RuntimeException("token非法"));}//使用userId从Redis缓存中获取用户信息String redisKey = "login:" + userId;SecurityUser securityUser = (SecurityUser)redisUtil.get(redisKey);if (Objects.isNull(securityUser)) {throw new RuntimeException("用户未登录");// 交给全局异常处理类处理
// resolver.resolveException(request, response, null,new RuntimeException("用户未登录"));}//将用户安全信息存入SecurityContextHolder, 在之后SpringSecurity的过滤器就不会拦截UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);}
}
7.创建SecurityConfig继承WebSecurityConfigurerAdapter
该类主要为Security配置类
package com.hng.config.jwtSecurity;import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import javax.annotation.Resource;/*** @Author: 郝南过* @Description: TODO* @Date: 2023/11/28 16:24* @Version: 1.0*/
@Configuration //注册为SpringBoot的配置类
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter {//注入Jwt认证拦截器.@Resourceprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;/*** 将BCryptPasswordEncoder加密器注入SpringSecurity中,* SpringSecurity的DaoAuthenticaionProvider会调用该加密器中的match()方法进行密码比对, 密码比对过程不需要我们干涉* @return*/@Beanpublic BCryptPasswordEncoder bcryptPasswordBean(){return new BCryptPasswordEncoder();}/*** 注入身份验证管理器, 直接继承即可.* @return* @throws Exception*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/*** 配置* @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http//禁用跨站请求伪造.csrf().disable()//禁用Session,使用token作为信息传递介质.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()//把token校验过滤器添加到过滤器链中, 添加在UsernamePasswordAuthenticationFilter之前是因为只要用户携带token,//就不需要再去验证是否有用户名密码了 (而且我们不使用表单登入, UsernamePasswordAuthenticationFilter是无法解析Json的, 相当于它没用了)//UsernamePasswordAuthenticationFilter是SpringSecurity默认配置的表单登录拦截器.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)// 认证请求的配置.authorizeRequests()// 将登入和注册的接口放开.antMatchers("/sys/login").anonymous().antMatchers("/sys/register").anonymous()//除了上面的那些, 剩下的任何接口请求都需要经过认证.anyRequest().authenticated().and()//允许跨域请求.cors();}}
8.在配置文件application.properties中添加相关的JWT配置信息
# 令牌自定义标识
jwt.token.header=token
# 令牌秘钥
jwt.token.secret=ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI=
# 令牌有效期(默认30分钟)
jwt.token.expireTime=1800
9.创建LoginController
package com.hng.controller;import com.hng.config.response.ResponseResult;
import com.hng.entity.SysUser;
import com.hng.service.SysUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;import java.util.List;/**
* <p>* 系统用户 前端控制器* </p>
*/
@Slf4j
@Api(value = "SysUser", tags = "SysUser")
@RestController
@RequiredArgsConstructor
@RequestMapping("/sys-user")
public class SysUserController {private final SysUserService sysUserService;@PostMapping("/getList")@ApiOperation("sysUser列表查询")public ResponseResult queryAllSysUser(@Validated @RequestBody SysUser sysUser){List<SysUser> list = sysUserService.queryAll(sysUser);return ResponseResult.success(list);}@PostMapping("/add")@ApiOperation("新增SysUser")public ResponseResult addSysUser(@Validated @RequestBody SysUser sysUser){sysUserService.addSysUser(sysUser);return ResponseResult.success();}/*** 根据ID查询数据* @param id ID*/@GetMapping("getById/{id}")@ApiOperation("sysUser根据Id查询")@ResponseBodypublic ResponseResult getById(@PathVariable Integer id) {return ResponseResult.success(sysUserService.getSysUserById(id));}/*** 更新数据* @param sysUser 实体对象*/@PutMapping("update")@ApiOperation("sysUser更新")@ResponseBodypublic ResponseResult update(@Validated @RequestBody SysUser sysUser) {sysUserService.updateSysUser(sysUser);return ResponseResult.success();}/*** 删除数据* @param id ID*/@DeleteMapping("delete/{id}")@ApiOperation("sysUser根据Id删除")@ResponseBodypublic ResponseResult delete(@PathVariable Integer id) {sysUserService.deleteSysUser(id);return ResponseResult.success();}}
五、测试
使用postman进行测试
1.login登录获取token
2.getList获取数据
将login中获取的token复制到headers中,请求测试
到此登录拦截已经可以正常使用了
六 、配置登录异常处理器
1.创建JwtAuthenticationEntryPoint实现AuthenticationEntryPoint
该类主要用来处理认证失败异常
package com.hng.config.security;import com.alibaba.fastjson.JSONObject;
import com.hng.config.response.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** @Author: 郝南过* @Description: 认证失败处理器* @Date: 2023/12/19 17:17* @Version: 1.0*/
@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request,HttpServletResponse response,AuthenticationException authException) throws IOException {// 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpStatus.OK.value());response.getWriter().write(JSONObject.toJSONString(ResponseResult.fail(HttpStatus.UNAUTHORIZED.value(),"认证失败")));response.getWriter().flush();}
}
2.创建JwtAccessDeniedHandler实现AccessDeniedHandler
该类主要用于处理鉴权失败异常
package com.hng.config.security;import com.alibaba.fastjson.JSONObject;
import com.hng.config.response.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** @Author: 郝南过* @Description: 鉴权失败处理* @Date: 2023/12/19 17:26* @Version: 1.0*/
@Component
@Slf4j
public class JwtAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {//当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpStatus.OK.value());response.getWriter().write(JSONObject.toJSONString(ResponseResult.fail(HttpStatus.FORBIDDEN.value(),"鉴权失败")));response.getWriter().flush();}
}
3.全局异常处理器GlobalExceptionHandler中配置以上俩个异常
/*** 全局捕获security的权限不足异常*/@ExceptionHandler(value = AccessDeniedException.class)public void accessDeniedException(AccessDeniedException e) {log.error("权限不足异常!原因是:[{}]", e.getMessage());throw e;}/*** 全局捕获security的认证失败异常*/@ExceptionHandler(value = AuthenticationException.class)public void authenticationException(AuthenticationException e) {log.error("用户认证失败异常!原因是:[{}]", e.getMessage());throw e;}
4.在SecurityConfig中配置以上俩个异常
@Resourceprivate JwtAuthenticationEntryPoint authenticationEntryPoint;@Resourceprivate JwtAccessDeniedHandler accessDeniedHandler;
//配置异常处理器.exceptionHandling()//认证失败处理器.authenticationEntryPoint(authenticationEntryPoint)//鉴权失败处理器.accessDeniedHandler(accessDeniedHandler)
5.测试
认证失败异常可使用错误的用户名密码登录进行测试
鉴权失败异常,暂时不能测试,需要补全权限代码后才可以进行测试
【demo示例代码】
https://download.csdn.net/download/shaogaiyue9745602/88661442https://download.csdn.net/download/shaogaiyue9745602/88661442