一、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;
}
@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;
}
}
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 {
@Resource
private SysUserService sysUserService;
@Override
public 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 {
@Resource
private 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 {
@Resource
private RedisUtil redisUtil;
// JWT 工具类
@Resource
private JwtTokenUtil jwtTokenUtil;
@Resource
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
/**
* 从请求中获取 JWT 令牌,并根据令牌获取用户信息,最后将用户信息封装到 Authentication 中,
* 方便后续校验(只会执行一次)
* @param request
* @param response
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//token为空的话, 就不管它, 让SpringSecurity中的其他过滤器处理请求,请求放行
filterChain.doFilter(request, response);
return;
}
//token不为空时, 解析token
String userId = null;
try {
Claims claims = jwtTokenUtil.getClaimsFromToken(token);
//解析出userId
userId = 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认证拦截器.
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 将BCryptPasswordEncoder加密器注入SpringSecurity中,
* SpringSecurity的DaoAuthenticaionProvider会调用该加密器中的match()方法进行密码比对, 密码比对过程不需要我们干涉
* @return
*/
@Bean
public BCryptPasswordEncoder bcryptPasswordBean(){
return new BCryptPasswordEncoder();
}
/**
* 注入身份验证管理器, 直接继承即可.
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置
* @param http
* @throws Exception
*/
@Override
protected 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查询")
@ResponseBody
public ResponseResult getById(@PathVariable Integer id) {
return ResponseResult.success(sysUserService.getSysUserById(id));
}
/**
* 更新数据
* @param sysUser 实体对象
*/
@PutMapping("update")
@ApiOperation("sysUser更新")
@ResponseBody
public ResponseResult update(@Validated @RequestBody SysUser sysUser) {
sysUserService.updateSysUser(sysUser);
return ResponseResult.success();
}
/**
* 删除数据
* @param id ID
*/
@DeleteMapping("delete/{id}")
@ApiOperation("sysUser根据Id删除")
@ResponseBody
public 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 {
@Override
public 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 {
@Override
public 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中配置以上俩个异常
@Resource
private JwtAuthenticationEntryPoint authenticationEntryPoint;
@Resource
private JwtAccessDeniedHandler accessDeniedHandler;
//配置异常处理器
.exceptionHandling()
//认证失败处理器
.authenticationEntryPoint(authenticationEntryPoint)
//鉴权失败处理器
.accessDeniedHandler(accessDeniedHandler)
5.测试
认证失败异常可使用错误的用户名密码登录进行测试
鉴权失败异常,暂时不能测试,需要补全权限代码后才可以进行测试
【demo示例代码】
https://download.csdn.net/download/shaogaiyue9745602/88661442https://download.csdn.net/download/shaogaiyue9745602/88661442