前言:最近在学习SpringSecurity的过程中,参考了很多网上的教程,同时也参考了一些目前主流的开源框架,于是结合自己的思路写了一个SpringBoot整合SpringSecurity+JWT+Redis完整的项目,从0到1写完感觉还是收获到不少的,于是我把我完整的笔记写成博客分享给大家,算是比较全的一个项目了,仅供大家参考和学习哦!
一、SpringSecurity简介
SpringSecurity是Spring生态系统中的安全管理框架,提供了一套Web应用安全性的完整解决方案。
它具有以下特点:
1、全面性:SpringSecurity提供了认证、授权、攻击防护等安全管理的全部功能。
2、扩展性:可以通过继承类、实现接口等方式轻松扩展SpringSecurity的功能。
3、与Spring无缝集成:可以与Spring框架完美整合,通过SpringIoC容器管理SpringSecurity组件。
4、防范常见攻击:可以防止脚本注入、会话固定、SQL注入等常见Web攻击。
5、配置简单:通过配置文件可以快速应用SpringSecurity带来的安全功能。
SpringSecurity的主要功能包括:
1、认证(Authentication):验证用户身份信息的合法性。
2、授权(Authorization):验证用户是否有权限执行操作。
3、防护攻击:防御如CSRF、Session固定、SQL注入等攻击。
4、方法安全:实现与系统方法的安全访问控制。
5、安全响应头:添加浏览器安全相关的响应头,提高安全性。
综上,SpringSecurity是一个MVC应用不可或缺的安全防护框架,为Java应用提供全面的安全支持。它与Spring框架集成紧密,配置简单,使用方便。
二、SpringSecurity认证流程
Spring Security认证流程中的几个核心类及其作用如下:
1、Authentication:认证信息接口,表示当前用户的认证信息,通常使用UsernamePasswordAuthenticationToken作为实现。
2、AuthenticationManager:认证管理器接口,authenticate()方法用来执行认证流程。
3、ProviderManager:认证管理器接口常用实现,封装多个AuthenticationProvider。
4、AuthenticationProvider:具体的认证处理器,由它完成特定的认证机制。
5、UserDetailsService:根据用户名加载用户信息,返回UserDetails接口的实现。
6、UserDetails:包含用户信息的接口,框架中代表用户信息。
7、UsernamePasswordAuthenticationFilter:处理表单登录认证的过滤器。
8、AbstractAuthenticationProcessingFilter:认证处理过滤器基类。
9、SecurityContextHolder:安全上下文容器,存取Authentication对象。
这是完整的SpringSecurity的认证流程:
1、用户向系统提交用户名和密码进行认证。
2、AuthenticationFilter会拦截请求,并从请求中提取出用户名和密码构造一个UsernamePasswordAuthenticationToken。
3、AuthenticationFilter将UsernamePasswordAuthenticationToken传入AuthenticationManager。
4、AuthenticationManager会找到一个匹配的AuthenticationProvider来进行认证。
5、AuthenticationProvider会先调用UserDetailsService的loadUserByUsername()方法根据用户名加载用户信息。
6、UserDetailsService根据用户名查询数据库,构造出一个UserDetails对象,包含用户信息、权限等。
7、AuthenticationProvider使用UserDetails和用户输入的密码进行匹配验证。如果匹配上就验证成功。
8、如果验证成功,AuthenticationProvider会构造一个已认证的Authentication对象。
9、AuthenticationProvider返回Authentication给AuthenticationManager。
10、AuthenticationManager将Authentication设置到SecurityContextHolder中。
11、后续的访问控制将使用SecurityContextHolder中的Authentication信息来验证用户身份和权限。
12、登录成功,用户访问系统受保护资源。
完整流程如图所示:
三、问题记录(重要)
阅读下面请务必先了解一下这个报错:
autoType is not support.org.springframework.security.core.authority.SimpleGrantedAuthority错误记录(亲测可用)
四、项目核心代码讲解
因为代码量比较庞大,所以我把整个项目的关键代码单独拿出来进行讲解,其他的次要的就不贴出来了,主要还是为了能够让大家更通俗易懂的去了解SpringSecurity的执行过程,完整的代码我会开源到Gitee,提供在文章的结尾。
4.1、导入pom依赖
完整的依赖都贴出来了。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok依赖包 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
<scope>provided</scope>
</dependency>
<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- redis依赖 对象池 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- pool 对象池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>
<!-- 常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.22</version>
</dependency>
</dependencies>
4.2、yml配置文件
主要配置了一个Redis连接以及Token常量。
spring:
redis:
host: localhost
port: 6379
database: 0
password:
timeout: 10s
lettuce:
pool:
min-idle: 0
max-idle: 8
max-active: 8
max-wait: -1ms
token:
header: Authorization
secret: oqwe9sdladwosqwqs
expireTime: 30
4.3、实体类
一共涉及了四个实体类,主要设计了一些关键的字段,并不是非常完整。
4.3.1、LoginBody登录实体类
这个类主要用于接收前端传递过来的用户名和密码,然后去验证登录信息用的。
完整代码:
package com.example.security.domain;
import lombok.Data;
@Data
public class LoginBody
{
/**
* 用户名
*/
private String username;
/**
* 用户密码
*/
private String password;
}
4.3.2、Role角色类
存放每个用户的角色信息,需要实现序列化接口。
完整代码:
package com.example.security.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
@Data
@AllArgsConstructor
public class Role implements Serializable {
/**
* 角色主键
*/
private Long id;
/**
* 角色名称
*/
private String name;
}
4.3.3、User用户类
主要存放的是用户的信息,需要实现序列化接口。
完整代码:
package com.example.security.domain;
import lombok.Data;
import java.io.Serializable;
import java.util.Set;
@Data
public class User implements Serializable {
/**
* 主键
*/
private String id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 角色集合
*/
private Set<Role> roles;
}
4.3.4、LoginUser登录用户信息
需要实现SpringSecurity自带的UserDetails接口,并实现它所有的方法,在Spring Security中,我们可以通过GrantedAuthority接口来表示一个用户所拥有的权限。
方法解释isAccountNonExpired()账号是否已过期isAccountNonLocked()账号是否已锁定isCredentialsNonExpired()凭(密码)是否已过期isEnabled()账号是否可用
这些方法返回true的目的是简化逻辑,在没有实现对应状态判断时,默认设置为true,这样可以避免不必要的认证/授权失败。
完整代码:
package com.example.security.domain;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Set;
@Data
public class LoginUser implements UserDetails {
public LoginUser(User user,Set<GrantedAuthority> authorities)
{
this.user = user;
this.authorities = authorities;
}
/**
* 用户信息
*/
private User user;
/**
* 权限信息
*/
private Set<GrantedAuthority> authorities;
/**
* token信息
*/
private String token;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@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;
}
}
4.4、TokenService服务类
先注入一些参数。
关键代码:
@Value("${token.header}")
private String header;
@Value("${token.secret}")
private String secret;
@Value("${token.expireTime}")
private int expireTime;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
4.4.1、生成令牌核心代码
关键代码:
private String generateToken(Map<String, Object> claims)
{
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
4.4.2、生成令牌关键逻辑
常量LOGIN_TOKEN_KEY:login_tokens:
1、首先生成一个随机的UUID作为token的值,设置到LoginUser对象中。
2、设置LoginUser的登录时间和过期时间(当前时间 + 过期时间)。
3、将LoginUser对象存储到Redis中,key为LOGIN_TOKEN_KEY+token,过期时间默认为yml配置的30分钟。
4、最终调用generateToken方法生成JWT token,传入claims哈希Map集合。
关键代码:
public String createToken(LoginUser loginUser)
{
String token = UUID.randomUUID().toString();
loginUser.setToken(token);
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
String userKey = CacheConstants.LOGIN_TOKEN_KEY + token;
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return generateToken(claims);
}
4.4.3、解析令牌核心代码
关键代码:
private Claims parseToken(String token)
{
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
4.4.4、获取请求头中携带的令牌
常量TOKEN_PREFIX:"Bearer "
1、从请求header中获取指定名称(yml文件配置的header)的authorization信息。
2、判断获得的token是否非空且以指定前缀(Constants.TOKEN_PREFIX)开头。
3、如果是,则移除前缀,得到最终的JWT token。
关键代码:
private String getToken(HttpServletRequest request)
{
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
{
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
4.4.5、获取Redis中存放的令牌Key
关键代码:
private String getTokenKey(String uuid)
{
return CacheConstants.LOGIN_TOKEN_KEY + uuid;
}
4.4.6、刷新令牌有效期
1、参数loginUser是当前登录的用户信息。
2、设置loginUser的新的登录时间为当前时间。
3、重新计算过期时间为当前时间+过期时间(yml文件配置的expireTime)。
4、根据登录用户的token作为key,存储更新后的loginUser到Redis中,并设置过期时间。
5、这样就相当于刷新了token的过期时间。
关键代码:
public void refreshToken(LoginUser loginUser)
{
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
4.4.7、验证令牌有效期
验证令牌有效期,相差不足20分钟,自动刷新缓存。
关键代码:
public void verifyToken(LoginUser loginUser)
{
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
{
refreshToken(loginUser);
}
}
4.4.8、获取用户身份信息
1、先从请求中获取JWT Token。
2、如果Token不为空,则对Token进行解析,获取claims。
3、从claims中取出对应的uuid。
4、根据uuid作为key,从Redis中获取LoginUser对象。
5、如果获取成功,返回LoginUser对象。
6、如果解析token或获取用户失败,则返回null。
关键代码:
public LoginUser getLoginUser(HttpServletRequest request)
{
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
{
try
{
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
return user;
}
catch (Exception e)
{
}
}
return null;
}
4.4.9、删除用户身份信息
通过token获取用户在redis中的唯一标识进行清除用户的登录数据,下次走过滤器的时候就会因没有此用户信息进行限制访问了!
/**
* 删除用户身份信息
*/
public void delLoginUser(String token)
{
if (StringUtils.isNotEmpty(token))
{
String userKey = getTokenKey(token);
redisCache.deleteObject(userKey);
}
}
4.5、AuthenticationEntryPointImpl配置认证失败处理类
常量UNAUTHORIZED:401
AuthenticationEntryPoint是SpringSecurity中用于处理认证失败的接口,用于未登录或登录过期的情况,会触发commence方法。
1、在方法内部,首先设置了响应状态码为401 Unauthorized。
2、然后使用StringUtils生成了一个错误信息字符串,包含请求访问的接口路径和认证失败的提示。3、最后使用AjaxResult把状态码和错误信息封装成一个结果,通过ServletUtils以JSON格式写入响应中。
4、AjaxResult是一个封装AJAX请求结果的类,可以方便地生成错误或成功的响应结果。
5、ServletUtils是一个工具类,可以方便地将String数据渲染到HttpServletResponse中。
所以这个类的作用就是在认证失败时,以JSON格式返回一个包含错误代码和消息的结果到前端,前端可以根据这个结果显示对应提示或做处理。
关键代码:
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint
{
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
int code = HttpStatus.UNAUTHORIZED;
String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}
4.6、JwtAuthenticationTokenFilter认证过滤器
OncePerRequestFilter是SpringSecurity提供的一个过滤器基类,主要用于保证过滤器在一个请求内只执行一次,JwtAuthenticationTokenFilter需要继承这个基类并重写doFilterInternal的方法。
1、通过tokenService从请求头中提取JWT token,并解析得到LoginUser对象
2、调用tokenService的verifyToken方法验证JWT token的有效性
3、使用LoginUser对象构建一个UsernamePasswordAuthenticationToken
4、设置AuthenticationToken的细节,如请求来源等
5、将构造好的UsernamePasswordAuthenticationToken对象设置到SecurityContextHolder的Context中。
6、这样登录用户的Authentication对象就保存到了安全上下文中。
7、最后过滤器链继续向后执行doFilter方法。
这样在过滤器中就实现了对token的解析和验证,并设置了Authentication对象到安全上下文中,
后续的过滤器就可以依据它来判断用户认证信息了。
关键代码:
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Value("${token.header}")
private String header;
@Value("${token.secret}")
private String tokenKey;
@Resource
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (loginUser != null)
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
** 注:如果你要获取当前登录的用户信息,可以通过以下2行代码进行获取。**
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
4.7、FastJson序列化
package com.example.security.config;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
/**
* Redis使用FastJson序列化
*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
public FastJson2JsonRedisSerializer(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, JSONWriter.Feature.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, JSONReader.Feature.SupportAutoType);
}
}
4.8、自定义Redis序列化
注:详细讲解在这篇文章:SpringBoot整合RedisTemplate操作Redis数据库详解(提供Gitee源码)
package com.example.security.config;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
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;
/**
* redis配置
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
String[] acceptNames = {"org.springframework.security.core.authority.SimpleGrantedAuthority"};
GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer(acceptNames);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
4.9、Redis工具类
package com.example.redis.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* spring redis 工具类
**/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@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 Redis键
* @return 有效时间
*/
public long getExpire(final String key)
{
return redisTemplate.getExpire(key);
}
/**
* 判断 key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key)
{
return redisTemplate.hasKey(key);
}
/**
* 获得缓存的基本对象。
*
* @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 boolean deleteObject(final Collection collection)
{
return redisTemplate.delete(collection) > 0;
}
/**
* 缓存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 Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 删除Hash中的某条数据
*
* @param key Redis键
* @param hKey Hash键
* @return 是否成功
*/
public boolean deleteCacheMapValue(final String key, final String hKey)
{
return redisTemplate.opsForHash().delete(key, hKey) > 0;
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
/**
* 存储有序集合
* @param key 键
* @param value 值
* @param score 排序
*/
public void zSet(Object key, Object value, double score){
redisTemplate.opsForZSet().add(key, value, score);
}
/**
* 存储值
* @param key 键
* @param set 集合
*/
public void zSet(Object key, Set set){
redisTemplate.opsForZSet().add(key, set);
}
/**
* 获取key指定范围的值
* @param key 键
* @param start 开始位置
* @param end 结束位置
* @return 返回set
*/
public Set zGet(Object key, long start, long end){
Set set = redisTemplate.opsForZSet().range(key, start, end);
return set;
}
/**
* 获取key对应的所有值
* @param key 键
* @return 返回set
*/
public Set zGet(Object key){
Set set = redisTemplate.opsForZSet().range(key, 0, -1);
return set;
}
/**
* 获取对用数据的大小
* @param key 键
* @return 键值大小
*/
public long zGetSize(Object key){
Long size = redisTemplate.opsForZSet().size(key);
return size;
}
}
4.10、SecurityConfig核心配置类
这是核心代码,注释都在代码上面了,可以自己仔细看一下,这边就不多做阐述。
完整代码:
package com.example.security.config;
import com.example.security.filter.JwtAuthenticationTokenFilter;
import com.example.security.service.serviceImpl.AuthenticationEntryPointImpl;
import com.example.security.service.serviceImpl.LogoutSuccessHandlerImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;
import javax.annotation.Resource;
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
/**
* token认证过滤器
*/
@Resource
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 认证失败处理类
*/
@Resource
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Resource
private LogoutSuccessHandlerImpl logoutSuccessHandler;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
//允许登录接口匿名访问
.antMatchers("/login").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加Logout filter
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
4.11、AuthenticationContextHolder线程本地的存储
主要提供了以下几个静态方法:
1、getContext()获取当前线程的Authentication对象。
2、setContext(Authentication context)设置当前线程的Authentication对象。
3、clearContext()清除当前线程的Authentication对象。
它使用ThreadLocal维护线程隔离,所以每个线程拥有自己的Authentication信息,互不干扰。
在Spring Security中,可以通过该类在不同层传递认证信息。
关键代码:
public class AuthenticationContextHolder
{
private static final ThreadLocal<Authentication> contextHolder = new ThreadLocal<>();
public static Authentication getContext()
{
return contextHolder.get();
}
public static void setContext(Authentication context)
{
contextHolder.set(context);
}
public static void clearContext()
{
contextHolder.remove();
}
}
4.12、UserServiceImpl查询用户接口
这边我偷懒了没有连接数据库,这个密码是通过如下代码加密获得的。
关键代码:
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123456");
System.out.println(encode);
主要是为了演示,所以这边直接把用户信息和角色信息写死了,实际开发还是需要连接数据库的。
关键代码:
@Service
public class UserServiceImpl {
public User selectUserByUsername(String username){
User user = new User();
user.setId(UUID.randomUUID().toString());
user.setUsername(username);
user.setPassword("$2a$10$ErrO7WgkEBAWVQwuJtbBve7R2.pSKUrfs7zt8XkASqJKqcetMvAUC");
Set<Role> roles = new HashSet<>();
Role role1 = new Role(1L, "ROLE_ADMIN");
Role role2 = new Role(2L, "ROLE_USER");
roles.add(role1);
roles.add(role2);
user.setRoles(roles);
return user;
}
}
这边用户必须以ROLE_作为前缀,具体分析看3.15的@PreAuthorize注解分析。
4.13、PasswordServiceImpl密码验证服务类
1、validate()方法用来验证用户密码。
2、它先从AuthenticationContextHolder中获取当前认证的用户名和密码。
3、然后调用matches()方法来校验密码。
4、matches()方法使用BCryptPasswordEncoder对存储的密文密码进行匹配验证。
5、如果匹配成功则验证成功,失败则验证失败。
这样通过Spring Security的AuthenticationContextHolder可以获取到当前认证principal的信息。
再结合密码加密匹配验证,就可以在服务中方便的实现密码的验证。
关键代码:
@Service
public class PasswordServiceImpl {
public void validate(User user)
{
Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
String AuthUsername = usernamePasswordAuthenticationToken.getName();
String AuthPassword = usernamePasswordAuthenticationToken.getCredentials().toString();
if (matches(user, AuthPassword)) {
System.out.println("验证成功!");
} else {
System.out.println("验证失败!");
}
}
public boolean matches(User user, String rawPassword)
{
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.matches(rawPassword, user.getPassword());
}
}
4.14、UserDetailsServiceImpl认证用户服务类
1、实现Spring Security的UserDetailsService接口。
UserDetailsService是Spring Security用于加载用户信息的核心接口。自定义实现可以灵活控制用户信息加载过程。
2、根据用户名加载用户信息通过userService查询数据库获取用户对象,包含用户信息如用户名、密码、角色等。
3、验证用户密码使用passwordService进行密码验证,校验登录的密码是否正确。
4、构建用户权限信息将用户的角色信息转换成GrantedAuthority授权信息集合。
5、封装用户对象返回将用户信息、权限信息封装到LoginUser对象中返回作为UserDetails。
6、在登录验证时提供用户详细信息Spring Security在登录验证时会调用此服务获取用户详细信息,以进行认证和授权。
关键代码:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private PasswordServiceImpl passwordService;
@Resource
private UserServiceImpl userService;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userService.selectUserByUsername(username);
passwordService.validate(user);
//取出角色和权限信息
Set<Role> roles = user.getRoles();
Set<GrantedAuthority> authorities = new HashSet<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return new LoginUser(user,authorities);
}
}
4.15、LoginController登录接口
所有的登录都必须走这个接口,登录成功以后会返回给用户一个令牌,以访问系统受保护的资源。
关键代码:
@RestController
public class LoginController {
@Resource
private LoginServiceImpl loginService;
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword());
ajax.put(Constants.TOKEN, token);
return ajax;
}
}
4.16、LoginServiceImpl登录接口核心逻辑
1、创建UsernamePasswordAuthenticationToken,包含用户名和密码。
2、将该authenticationToken设置到AuthenticationContextHolder,这是Spring Security提供的一个存储authentication的holder。
3、调用AuthenticationManager的authenticate方法进行认证。这个方法会根据配置调用相关的UserDetailsService等进行认证。(下面会走认证用户信息服务类中的loadUserByUsername方法),验证成功以后会返回一个authentication对象。
4、清空AuthenticationContextHolder。
5、从authentication对象中获取登录用户信息LoginUser。
6、使用TokenService生成JWT token。
7、返回JWT token。
关键代码:
@Service
public class LoginServiceImpl {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private TokenService tokenService;
public String login(String username, String password)
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
AuthenticationContextHolder.clearContext();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
return tokenService.createToken(loginUser);
}
}
4.17、LogoutSuccessHandlerImpl退出登录核心逻辑
LogoutSuccessHandlerImpl实现了Spring Security的LogoutSuccessHandler接口,用于处理用户退出登录成功后的逻辑。
1、通过TokenService获取当前登录的用户信息LoginUser。
2、如果LoginUser不为空,则调用TokenService的delLoginUser方法删除该用户的缓存信息。这里涉及到一个自定义的TokenService,用于处理基于令牌的用户认证。登录时生成一个令牌与用户信息对应,退出时需要删除这个缓存关系。
3、使用ServletUtils向响应写入退出成功的提示,这样ajax请求可以获取到提示信息。
关键代码:
/**
* 退出登录
*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
@Autowired
private TokenService tokenService;
/**
* 退出处理
* @return
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser))
{
// 删除用户缓存记录
tokenService.delLoginUser(loginUser.getToken());
}
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功")));
}
}
4.18、@PreAuthorize注解
这个注解是打在接口上的,用于标记该接口只有指定的用户才有权限访问。
1、在接口上使用**@PreAuthorize("hasRole('USER')")**这个注解时候,内部源码执行的流程我可以给大家剖析一下。
首先获取上面我们配置好的接口参数为USER。
进入hasAnyAuthorityName方法,获取当前用户的所有权限(roleSet集合),然后用for循环去遍历注解上的参数数组,这边只循环了一次,因为注解上只配了一个USER,所以数组长度为1,如果当前用户的所有权限集合中包含当前接口的权限信息则放行,否则不放行。
3、getRoleWithDefaultPrefix方法是用来检测当前传递进来的role是否以默认的**ROLE_为前缀,如果有则直接返回,没有的话再拼上ROLE_**的默认前缀进行返回。
这就是这个注解的验证过程,以此也解释了3.9中为什么用户权限必须以**ROLE_**为前缀打头的原因。
4.19、HelloController测试接口
分别写个2个测试接口,一个是登陆后谁都可以访问的接口,一个是登陆后需要USER权限的用户才可以访问的接口,还有一个是登录后需要COMMON权限才可以访问的接口。
关键代码:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "Hello World!";
}
@GetMapping("/user")
@PreAuthorize("hasRole('USER')")
public String user(){
return "Hello USER!";
}
@GetMapping("/common")
@PreAuthorize("hasRole('COMMON')")
public String common(){
return "Hello COMMON!";
}
}
注意:必须用public方法修饰,否则@PreAuthorize注解失效!
4.20、总结
最后我们通过实际的项目来总结一下本次SpringSecurity的认证流程:
1、在LoginService的login方法中,构造一个UsernamePasswordAuthenticationToken,包含用户名和密码,这是开始认证的入口。
2、LoginService调用AuthenticationManager的authenticate方法启动认证流程。
3、AuthenticationManager会找到一个匹配的AuthenticationProvider来进行认证。
4、AuthenticationProvider会调用UserDetailsService的loadUserByUsername方法加载用户信息。这里我们通过UserDetailsServiceImpl来查询用户。
5、在UserDetailsServiceImpl中,根据用户名查询用户信息,然后调用PasswordService进行密码验证。
6、PasswordService通过AuthenticationContextHolder获取登录的用户名和密码。然后与数据库中存储的用户密码(经过编码)进行匹配,如果匹配上就验证成功。
7、PasswordService验证成功后,UserDetailsServiceImpl将根据用户信息构造一个UserDetails对象(这里是LoginUser),包含了用户名,密码,权限信息等。
8、UserDetailsServiceImpl将UserDetails返回给AuthenticationProvider。
9、AuthenticationProvider收到UserDetails后,完成验证,并生成一个已认证的Authentication对象。
10、AuthenticationProvider将Authentication返回给AuthenticationManager。
11、AuthenticationManager设置该Authentication到SecurityContextHolder中,供后续访问控制使用。
12、LoginService拿到已认证的Authentication,从中取出UserDetails,生成JWTtoken并返回。
综上,结合项目的逻辑SpringSecurity的认证流程大体可以分为:获取用户信息->用户验证->构建UserDetails->生成Authentication。我们通过自定义UserDetailsService和PasswordService来实现了用户验证逻辑。
五、运行项目
5.1、登录成功
通过post请求发送json格式的数据进行登录。
登录成功了并返回了Token令牌!
5.2、访问无权限接口
把刚才获得的令牌设置到请求头当中,进行访问测试接口。
5.3、访问需要USER权限的接口
把hello改成user。
因为这个用户默认配置了ADMIN和USER两个权限,所以可以访问成功!
5.4、访问需要COMMON权限的接口
把user改成common。
很明显,返回了403的错误信息。
5.5、退出登录
将common改成logout。
退出登录成功。
5.6、访问失败
上面退出登录了,看看还能不能通过携带之前的token访问测试接口。
可以很明显的看到,系统返回了401的错误。
5.7、登录失败
上面我们都是登录成功的例子,我们这次故意输错密码,看看能不能登录成功。
可以很明显的看到,系统返回了401的错误。
六、Gitee源码地址
因为本篇博客提供的代码不是完整的,所以我把完整的项目开源到了码云上,供大家学习和参考!
项目地址:SpringBoot整合SpringSecurity+JWT+Redis完整教程
七、总结
以上就是我对于SpringSecurity以及如何在实际项目当中开发应用的个人理解,如有问题欢迎评论区留言!
版权归原作者 黄团团 所有, 如有侵权,请联系我们删除。