前言
本文将着重介绍在Spring Security中如何使用JWT作为数据访问的身份认证令牌,使用JWT这个将代替原来的cookie身份认证。
一、流程分析
在正式开始前,我们先对Spring Security 的登录流程进行分析
在我们登录成功后,就需要生成JWT令牌,并将其返回给前端。前端需要访问某个资源时,必须携带token,后端就需要在访问资源前对判断是否携带JWT令牌,令牌是否有效。
那么接下来的问题就是认证流程的问题了,也就是一个用户是否在我们设定的数据源中,假如存在且用户为启动状态,则登录成功,执行上述流程,反之登录失败,返回失败信息。
我们来看看,security中的整个认证流程:
在整个认证流程中,我们在提交用户表单数据的时候,会先被UsernamePasswordAuthenticationFilter捕获到如何被封装为UsernamePasswordAuthenticationToken 的token 对象,然后将整个对象交给AuthenticationManager认证管理器去实现认证,而这个时候就会去数据源UserDetailService类中去进行对应的认证方式,认证成功返回UserDetails对象,整个对象还会被进一步封装为Authentication对象返回给AuthenticationManager。
到这里我们的目的就比较明确了, 我们只需要重新修改两个流程步骤即可实现JWT的生成和身份认证,第一,是修改UsernamePasswordAuthenticationFilter过滤链,使这条过滤链拥有对JWT的判断功能,二是实现认证成功后生成JWT并返回给前端同时将这个对象信息存储进redis作为缓存
二、准备工作
1.配置Redis
(1)引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.25</version>
</dependency>
(2)Redis配置文件和工具类
@Component
public class RedisCache
{
@Autowired
public 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);
}
}
@Configuration
public 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;
}
}
/**
* 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;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public 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);
}
}
(3)application.yaml配置Redis
spring:
redis:
host: 服务器地址
port: 6379 #Redis默认端口
password: 密码
lettuce:
pool:
max-active: 8
# 最大阻塞等待时间(使用负数表示没有限制)
max-wait: 1ms
# 最大空闲链接
max-idle: 8
# 最小空闲链接
min-idle: 0
2.配置JWT
(1)引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.5</version>
</dependency>
(2) JWT工具类
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "xxx";
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("hzj") // 签发者
.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();
}
/**
* 生成加密后的秘钥 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();
}
}
3.数据源配置
(1)定义User类继承自UserDetails
@Data
public class User implements UserDetails {
private Long id;
private String username;
private String password;
private String nickname;
private boolean enabled;
@TableField(exist = false)
private List<Role> roles;
private String email;
private String userface;
@TableField(value = "reg_time")
private Timestamp regTime;
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
// 创建角色集合
Set<GrantedAuthority> grantedRoles = new HashSet<>();
// 遍历实例对象所拥有的角色链表
for (Role role : roles) {
// 将每个角色封装为 授权角色对象,
grantedRoles.add(new SimpleGrantedAuthority(UserRoles.isWho(role.getRid())));
}
return grantedRoles;
}
@Override
@JsonIgnore
public boolean isAccountNonExpired() {
return true;
}
@Override
@JsonIgnore
public boolean isAccountNonLocked() {
return true;
}
@Override
@JsonIgnore
public boolean isCredentialsNonExpired() {
return true;
}
@Override
@JsonIgnore
public boolean isEnabled() {
return true;
}
}
public final class UserRoles {
public static final Long ADMIN = Long.valueOf(1);
public static final Long USER = Long.valueOf(2);
public static final Long TEST_USER_1 = Long.valueOf(3);
public static final Long TEST_USER_2 = Long.valueOf(4);
public static final Long TEST_USER_3 = Long.valueOf(5);
public static final String ROLE_ADMIN = "ROLE_ADMIN";
public static final String ROLE_USER = "ROLE_ADMIN";
public static final String ROLE_TEST_USER_01 = "ROLE_TEST_USER_01";
public static final String ROLE_TEST_USER_02 = "ROLE_TEST_USER_02";
public static final String ROLE_TEST_USER_03 = "ROLE_TEST_USER_03";
public static String isWho(Long rid) {
if (Objects.equals(rid, ADMIN)) {
return ROLE_ADMIN;
}else
if (Objects.equals(rid, USER)) {
return ROLE_USER;
}else
if (Objects.equals(rid, TEST_USER_1)) {
return ROLE_TEST_USER_01;
}else
if (Objects.equals(rid, TEST_USER_2)) {
return ROLE_TEST_USER_02;
}
return ROLE_TEST_USER_03;
}
private UserRoles(){
}
}
(2) 基于数据库自定义数据源(使用MyBatis-plus)
@Component
public class LoginUserDetailService implements UserDetailsService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
@Autowired
public LoginUserDetailService(UserMapper userMapper, RoleMapper roleMapper) {
this.userMapper = userMapper;
this.roleMapper = roleMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("当前在自定义数据源中");
// 查询用户
LambdaQueryWrapper<User> queryWrapper_User = new LambdaQueryWrapper<>();
queryWrapper_User.eq(User::getUsername,username);
User user = userMapper.selectOne(queryWrapper_User);
// 工具类判断是否查询user对象是否有效
if (ObjectUtils.isEmpty(user)) {
throw new UsernameNotFoundException("用户不存在");
}
LambdaQueryWrapper<Role> queryWrapper_Role = new LambdaQueryWrapper<>();
queryWrapper_Role.eq(Role::getUid,user.getId());
List<Role> roles = roleMapper.selectList(queryWrapper_Role);
user.setRoles(roles);
return user;
}
}
4.Spring Security 配置
(1)JWT判断过滤类
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
System.out.println("无token直接进到过滤链");
filterChain.doFilter(request, response);
return;
}
System.out.println("有token获取认证信息");
//解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
// 获取jwt中的用户id
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
JSONObject loginUser = redisCache.getCacheObject(redisKey);
User user = loginUser.toJavaObject(User.class);
if(Objects.isNull(user)){
throw new RuntimeException("用户未登录");
}
System.out.println(token);
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
(4) Security配置类
@EnableWebSecurity
//@ApiModel(value = "博客Security自定义配置器")
public class BlogWebSecurityConfiger {
@Autowired
private LoginUserDetailService loginUserDetailService;
@Autowired
private AuthenticationConfiguration authenticationConfiguration;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 处理静态资源,使其图片,css样式等不需要认证就能获取
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
// Spring Security should completely ignore URLs starting with /resources/
.antMatchers("/resources/static/**");
}
// 自定义过滤链
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((auth) -> {
auth
.mvcMatchers("/user/login")
.permitAll()
.anyRequest()
.authenticated();
})
//不通过Session获取SecurityContext,关闭cookie
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin()
.and()
.logout()
.permitAll()
.and()
.csrf()
.disable();
// at: ⽤来某个 filter 替换过滤器链中哪个 filter
// before: 放在过滤器链中哪个 filter 之前
// after: 放在过滤器链中那个 filter 之后
http.addFilterAt(jwtAuthenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// 加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 获取AuthenticationManager(认证管理器),登录时认证使用。
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return this.authenticationConfiguration.getAuthenticationManager();
}
// 解决跨域问题
private CorsConfigurationSource configurationSource() {
CorsConfiguration corsConfiguration = new
CorsConfiguration();
// 设置接收cookie
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOriginPattern("*");
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new
UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",
corsConfiguration);
return source;
}
}
5.登录控制层及服务层类
(1) 服务层
public interface LoginServcie {
ResponseResult login(String username, String password);
ResponseResult logout();
}
```java
@Service
public class LoginServiceImpl implements LoginServcie {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(String username,String password) {
System.out.println("当前在service的login方法里面");
// 将username 和 password 封装成 用户 token 信息
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// 将封装token传非认证管理器进行认证,内部会访问定义的数据源来进行认证,认证成功则返回封装的Authentication对象,失败返回null
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (Objects.isNull(authenticate)) {
throw new RuntimeException("用户名或密码错误");
}
// 使用userid生成token
// 获取封装在Authentication对象内部的User对象
User loginUser = (User) authenticate.getPrincipal();
// 获取id
String userId = loginUser.getId().toString();
// 构造一个JWT 字符串
String jwt = JwtUtil.createJWT(userId);
//authenticate存入redis
redisCache.setCacheObject("login:" + userId, loginUser);
System.out.println(jwt);
//把token响应给前端
HashMap<String, String> map = new HashMap<>();
map.put("token", jwt);
return new ResponseResult(200, "登陆成功", map);
}
@Override
public ResponseResult logout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User loginUser = (User) authentication.getPrincipal();
Long userid = loginUser.getId();
redisCache.deleteObject("login:" + userid);
return new ResponseResult(200, "退出成功");
}
}
(2)控制层
@RestController
@RequestMapping("/user")
public class LoginController {
@Autowired
private LoginServcie loginServcie;
@PostMapping("/login")
public ResponseResult login(String username, String password){
System.out.println(username+"========="+password);
return loginServcie.login(username, password);
}
}
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "hello";
}
}
6.响应体
@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;
}
}
8.总览
三、测试
到这里基本工作就做完了,我们使用postman测试一下,发送表单登录数据
postman登录成功信息,后端给前端发送了token
后端反馈
查看redis内存储数据
可以看到存储了用户数据
接着我们继续测试带着Token去获取其他数据
这里需要主要的是设置请求头携带token,token的值为刚刚登录成功的JWT字符串
结果返回hello获取成功
后端反馈