SpringSecurity+JWT+redis认证流程
文章目录1. 整合SpringSecurity1.1 工作流程1.2 SpringSecurity的重要概念1. SecurityContext2. SecurityContextHolder3. Authentication4. AuthenticationManager1.3 引入Security与jwt2. 准备工作2.1 重写UserDetailsService2.2 Redis工具类2.
1. 整合SpringSecurity
1.1 工作流程
Spring Security
的设计思想:通过一层层的Filters来对web请求做处理。
如上图,一个请求想要访问到API就会以从左到右的形式经过蓝线框框里面的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分负责异常处理,橙色部分则是负责授权。
大概security认证方案如下:图片来自
流程说明:
-
客户端发起一个请求,进入 Security 过滤器链。
-
当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理。如果不是登出路径则直接进入下一个过滤器。
-
当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler ,登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
-
进入认证BasicAuthenticationFilter进行用户认证,成功的话会把认证了的结果写入到SecurityContextHolder中SecurityContext的属性authentication上面。如果认证失败就会交给AuthenticationEntryPoint认证失败处理类,或者抛出异常被后续ExceptionTranslationFilter过滤器处理异常,如果是AuthenticationException就交给AuthenticationEntryPoint处理,如果是AccessDeniedException异常则交给AccessDeniedHandler处理。
-
当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。
1.2 SpringSecurity的重要概念
-
SecurityContext:上下文对象,
Authentication
对象会放在里面。 -
SecurityContextHolder:用于拿到上下文对象的静态工具类。
-
Authentication:认证接口,定义了认证对象的数据形式。
-
AuthenticationManager:用于校验
Authentication
,返回一个认证完成后的Authentication
对象。
1. SecurityContext
上下文对象,认证后的数据就放在这里面,接口定义如下:
public interface SecurityContext extends Serializable {
// 获取Authentication对象
Authentication getAuthentication();
// 放入Authentication对象
void setAuthentication(Authentication authentication);
}
2. SecurityContextHolder
public class SecurityContextHolder {
public static void clearContext() {
strategy.clearContext();
}
public static SecurityContext getContext() {
return strategy.getContext();
}
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}
可以说是SecurityContext
的工具类,通常用于get、set、clear SecurityContext
,默认会把数据都存储到当前线程中。
3. Authentication
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
这几个方法效果如下:
-
getAuthorities
: 获取用户权限,一般情况下获取到的是用户的角色信息。 -
getCredentials
: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。 -
getDetails
: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)。 -
getPrincipal
: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails。 -
isAuthenticated
: 获取当前Authentication
是否已认证。 -
setAuthenticated
: 设置当前Authentication
是否已认证(true or false)。
Authentication
只是定义了一种在SpringSecurity进行认证过的数据的数据形式应该是怎么样的,要有权限,要有密码,要有身份信息,要有额外信息。
4. AuthenticationManager
public interface AuthenticationManager {
// 认证方法
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager
定义了一个认证方法,它将一个未认证的Authentication
传入,返回一个已认证的Authentication
,默认使用的实现类为:ProviderManager。
如此,将这四个部分串联起来,构成Spring Security进行认证的流程:
-
先是一个请求带着身份信息进来
-
经过
AuthenticationManager
的认证, -
再通过
SecurityContextHolder
获取SecurityContext
, -
最后将认证后的信息放入到
SecurityContext
。
1.3 引入Security与jwt
- pom.xml
<!-- spring-boot-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
2. 准备工作
数据库操作框架采用的是mybatis-plus
。
2.1 重写UserDetailsService
我们需要重新定义查用户数据的过程,重写UserDetailsService接口。
-
com.hdu.back.dto.LoginUser
UserDetails 是一个定义了数据形式的接口,用于保存我们从数据库中查出来的数据,其功能主要是验证账号状态和获取权限。
将用户信息和权限信息组装成一个UserDetails对象,重写UserDetails对象:
/**
* 登录用户身份权限
*
*/
public class LoginUser extends User implements UserDetails {
/**
* 用户唯一标识
*/
private String token;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
/**
* 权限列表
*/
private List<Permission> permissions;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public List<Permission> getPermissions() {
return permissions;
}
public void setPermissions(List<Permission> permissions) {
this.permissions = permissions;
}
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
return permissions.parallelStream().filter(p -> !StringUtils.isEmpty(p.getPermission()))
.map(p -> new SimpleGrantedAuthority(p.getPermission())).collect(Collectors.toSet());
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
// do nothing
}
// 账户是否未过期
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
// 账户是否未锁定
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
// 密码是否未过期
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 账户是否可用
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
public Long getLoginTime() {
return loginTime;
}
public void setLoginTime(Long loginTime) {
this.loginTime = loginTime;
}
public Long getExpireTime() {
return expireTime;
}
public void setExpireTime(Long expireTime) {
this.expireTime = expireTime;
}
}
- com.hdu.back.service.impl.UserDetailsServiceImpl
security在认证用户身份的时候会调用UserDetailsService.loadUserByUsername()方法,根据用户名查询用户对象。
/**
* UserDetailsService
* 用于在认证器中根据用户传过来的用户名查找一个用户
*/
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserService userService;
@Autowired
PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getOne(new QueryWrapper<User>().eq("username",username));
if (user == null) {
log.info("登录用户:{} 不存在.", username);
throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
}
LoginUser loginUser = new LoginUser();
BeanUtils.copyProperties(user, loginUser);
List<Permission> permissions = permissionService.getPermissions(user.getId());
loginUser.setPermissions(permissions);
return loginUser;
}
}
2.2 Redis工具类
- com.hdu.back.utils.RedisUtil
/**
* redis操作工具类
*/
@Component
public final class RedisUtil {
@Autowired
@Qualifier("myRedisTemplate")
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
*/
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((Collection<String>) 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)
*/
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)
*/
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
*/
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);
}
/**
* 获取hashKey对应的所有键
*
* @param key 键
* @return 对应的多个键
*/
public Set hmgetKey(String key) {
return redisTemplate.opsForHash().keys(key);
}
/**
* 获取hashKey对应的所有值
*
* @param key 键
* @return 对应的多个值
*/
public Collection hmgetValue(String key) {
return redisTemplate.opsForHash().values(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
*/
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表中放入数据,如果不存在将创建
*
* @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
* @return
*/
public Long hdel(String key, Object... item) {
return redisTemplate.opsForHash().delete(key, item);
}
/**
* 删除hash表
* @param hash 键 不能为null
* @return
*/
public Long hdel(String hash) {
return redisTemplate.opsForHash().delete(hash);
}
/**
* 获得hash表的长度
* @return
*/
public int hGetHashSize(String hash){
return redisTemplate.opsForHash().size(hash).intValue();
}
/**
* 判断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)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
*/
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 键
*/
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 到 -1代表所有值
*/
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 键
*/
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 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
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 值
*/
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 时间(秒)
*/
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
*/
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
*/
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;
}
}
}
2.3 ResponseUtil
- com.hdu.back.utils.ResponseUtil
public class ResponseUtil {
/**
* 获取request
*/
public static HttpServletRequest getRequest()
{
return getRequestAttributes().getRequest();
}
/**
* 获取response
*/
public static HttpServletResponse getResponse()
{
return getRequestAttributes().getResponse();
}
/**
* 获取session
*/
public static HttpSession getSession()
{
return getRequest().getSession();
}
public static ServletRequestAttributes getRequestAttributes()
{
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
}
/**
* 将data渲染到客户端
*
* @param response
* @param status
* @param data
*/
public static void responseJson(HttpServletResponse response, int status, Object data) {
try {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "*");
response.setContentType("application/json;charset=UTF-8");
response.setStatus(status);
response.getWriter().write(JSONObject.toJSONString(data));
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.4 解决跨域问题
- com.hdu.back.config.CorsConfig
/**
* 解决跨域问题
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 设置访问源地址
corsConfiguration.addAllowedOrigin("*");
// 设置访问源请求头
corsConfiguration.addAllowedHeader("*");
// 设置访问源请求方法
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 对接口配置跨域设置
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
2.5 TokenService
-
application.yml
添加token配置
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: V9RlaFCwZfxQ#PLJxRfKGU0IGNZKP7Kq
# 令牌有效期(默认30分钟)
expireTime: 1800
-
com.hdu.back.service.TokenService
Token实现类
基于JWT的认证模式,定义一个Token实现类,包括:创建Token、刷新Token、获取登录用户信息、删除登录用户信息、校验Token。
/**
* Token管理器
* 可存储到redis
* 基于redis
*/
public interface TokenService {
Token saveToken(LoginUser loginUser);
void refresh(LoginUser loginUser);
LoginUser getLoginUser(HttpServletRequest request);
void delLoginUser(String token);
void verifyToken(LoginUser loginUser);
}
- com.hdu.back.service.impl.TokenServiceImpl
@Component
public class TokenServiceImpl implements TokenService {
// 令牌自定义标识
@Value("${token.header}")
private String header;
// 令牌秘钥
@Value("${token.secret}")
private String secret;
// 令牌有效期(默认1800秒)
@Value("${token.expireTime}")
private int expireTime;
@Autowired
private RedisUtil redisUtil;
// 20分钟
private static final Long MILLIS_MINUTE_TWENTY = 20 * 60 * 1000L;
/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
@Override
public Token saveToken(LoginUser loginUser) {
String token = UUID.randomUUID().toString();
loginUser.setToken(token);
cacheLoginUser(loginUser);
String jwtToken = createJwtToken(loginUser);
return new Token(jwtToken, loginUser.getLoginTime());
}
/**
* 生成jwt
*
* @param loginUser
* @return
*/
private String createJwtToken(LoginUser loginUser) {
Map<String, Object> claims = new HashMap<>();
// 放入一个随机字符串,通过该串可找到登陆用户
claims.put(Constants.LOGIN_USER_KEY, loginUser.getToken());
String jwtToken = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
return jwtToken;
}
/**
* 缓存登录信息
*
* @param loginUser 登录信息
*/
private void cacheLoginUser(LoginUser loginUser) {
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * 1000);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisUtil.set(userKey,loginUser,expireTime);
}
private String getTokenKey(String uuid) {
return Constants.LOGIN_TOKEN_KEY + uuid;
}
/**
* 更新缓存的用户信息
*/
@Override
public void refresh(LoginUser loginUser) {
cacheLoginUser(loginUser);
}
/**
* 获取用户身份信息
*
* @return 用户信息
*/
@Override
public LoginUser getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token)) {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = (LoginUser) redisUtil.get(userKey);
return user;
}
return null;
}
/**
* 获取请求token
*
* @param request
* @return 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;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 设置用户身份信息
*/
public void setLoginUser(LoginUser loginUser) {
if (loginUser != null && StringUtils.isNotEmpty(loginUser.getToken())) {
refresh(loginUser);
}
}
/**
* 删除用户身份信息
*/
@Override
public void delLoginUser(String token) {
if (StringUtils.isNotEmpty(token)) {
String userKey = getTokenKey(token);
redisUtil.del(userKey);
}
}
/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param loginUser
* @return 令牌
*/
@Override
public void verifyToken(LoginUser loginUser){
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY){
refresh(loginUser);
}
}
}
3. 具体实现流程
3.1 Security配置
- com.hdu.back.config.SecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
AuthenticationSuccessHandler loginSuccessHandler;
@Autowired
AuthenticationFailureHandler loginFailureHandler;
@Autowired
LogoutSuccessHandler logoutSussHandler;
@Autowired
AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
UserDetailsService userDetailsService;
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
// 密码加密和验证策略
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// 定义白名单
public static final String[] URL_WHITELIST = {
"/webjars/**",
"/favicon.ico",
"/captcha",
"/login",
"/logout",
"/*.html",
"/**/*.html",
"/**/*.css",
"/swagger-ui.html",
"/swagger-resources/**",
"/v2/**",
"/**/*.js"
};
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF禁用,因为不使用session
http.csrf().disable()
// 解决不允许显示在iframe的问题
.headers().frameOptions().disable()
// 登录配置
.and()
.formLogin()
.failureHandler(loginFailureHandler)
.successHandler(loginSuccessHandler)
// 退出配置
.and()
.logout()
.logoutSuccessHandler(logoutSussHandler)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll() //白名单
.anyRequest().authenticated() // 除白名单外的所有请求全部需要鉴权认证
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint) // 认证失败处理类
// 基于token,所以不需要session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置自定义的过滤器
.and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* AuthenticationManager(认证管理器) 的建造器
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 把UserDetailsServiceImpl配置到SecurityConfig中
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
配置说明:
-
configure(AuthenticationManagerBuilder auth)
AuthenticationManager
的建造器,配置 AuthenticationManagerBuilder 会让Security 自动构建一个 AuthenticationManager;如果想要使用该功能,你需要配置一个UserDetailService
和PasswordEncoder
。UserDetailsService
用于在认证器中根据用户传过来的用户名查找一个用户,PasswordEncoder
用于密码的加密与比对,存储用户密码的时候用PasswordEncoder.encode() 加密存储,在认证器里会调用 PasswordEncoder.matches() 方法进行密码比对。这里采用spring security中的BCryptPasswordEncoder
加密算法,该算法采用SHA-256 +随机盐+密钥对密码进行加密,SHA系列是Hash算法,过程是不可逆的。如果重写了该方法,Security 会启用 DaoAuthenticationProvider 这个认证器,该认证就是先调用UserDetailsService.loadUserByUsername ,然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功则返回一个 Authentication 对象。
-
configure(HttpSecurity http)
见代码注释。
-
自定义 AuthenticationManager
解决无法直接注入 AuthenticationManager问题。
3.2 身份认证
1. 登录认证
首先,通过登录页面提交表单,由UsernamePasswordAuthenticationFilter
过滤器进行登陆操作处理。
- 通过attemptAuthentication 方法搜集参数(username、password),封装为验证请求
UsernamePasswordAuthenticationToken
,然后调用AuthenticationManager
进行身份认证。 AuthenticationManager
调用它的authenticate
方法进行认证,该方法会去调用UserDetailsServiceImpl.loadUserByUsername,然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功则返回一个认证完成的Authentication
对象。
登录成功之后会走AuthenticationSuccessHandler,登陆失败走AuthenticationFailureHandler。自定义登录成功操作类和登陆失败类:
- com.hdu.back.security.SecurityHandler
@Configuration
@Slf4j
public class SecurityHandler {
@Autowired
TokenService tokenService;
/**
* 登陆成功,返回Token
*
*/
@Bean
public AuthenticationSuccessHandler loginSuccessHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Token token = tokenService.saveToken(loginUser);
ResponseUtil.responseJson(response, HttpStatus.OK.value(), token);
}
};
}
/**
* 登陆失败
*
* @return
*/
@Bean
public AuthenticationFailureHandler loginFailureHandler() {
return new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String msg = null;
if (exception instanceof BadCredentialsException) {
msg = "用户名或密码错误";
} else {
msg = exception.getMessage();
}
ServerResponse info = new ServerResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg);
ResponseUtil.responseJson(response, HttpStatus.OK.value(), info);
}
};
}
}
登陆成功,authentication.getPrincipal()
获取当前登陆用户信息,创建jwt令牌,然后将loginUser存入redis中,并返回token给前端;登录失败,返回错误给前端。
2. jwt过滤器
前端获得token之后,放在请求头上
- src/utils/request.js
import axios from 'axios'
import {Notification, Message, MessageBox} from 'element-ui'
import {getToken} from '@/utils/auth'
import errorCode from '@/utils/errorCode'
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
// baseURL: process.env.VUE_APP_BASE_API,
baseURL: '/api',
// 超时
timeout: 10000
})
// request拦截器
service.interceptors.request.use(config => {
const token = getToken();
if (token) {
config.headers['Authorization'] = 'Bearer ' + token // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config;
}, error => {
console.log(error)
})
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.status || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
if (code === 401) {
Message({
message: msg,
type: "error"
})
} else if (code === 500) {
Message({
message: msg,
type: 'error'
})
return Promise.reject(new Error(msg))
} else if (code !== 200) {
Notification.error({
title: msg
})
return Promise.reject('error')
} else {
return res.data
}
},
error => {
console.log('err' + error)
let {message} = error;
if (message === "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
Message({
message: message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
})
export default service
后端需要将jwt过滤器放在过滤器链中,用于解析token,因为我们没有session,所以我们每次去辨别这是哪个用户的请求的时候,都是根据请求中的token来解析出来当前是哪个用户。
所以我们新建一个JwtAuthenticationTokenFilter
- com.hdu.back.security.JwtAuthenticationTokenFilter
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 获取当前登录用户信息
LoginUser loginUser = tokenService.getLoginUser(request);
if (loginUser != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 校验token是否过期
tokenService.verifyToken(loginUser);
// 组装authentication对象
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
// authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 将authentication信息放入到上下文对象中
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
过程说明:
-
拿到request头部对应的token信息,解析jwtToken,根据userKey从resdis中获取到用户信息。
-
查看获取到的
logUser
是否为null,UserDetail
是否保存于上下文中,以及查看token是否过期,。 -
组装一个
authentication
对象,把它放在上下文对象中,这样后面的过滤器看到我们上下文对象中有authentication
对象,就相当于我们已经认证过了。
3.3 认证失败和退出成功处理类
- com.hdu.back.security.SecurityHandler
@Configuration
@Slf4j
public class SecurityHandler {
@Autowired
TokenService tokenService;
/**
* 认证失败处理类 返回未授权401
*
* @return
*/
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
ServerResponse info = new ServerResponse(HttpStatus.UNAUTHORIZED.value(), "认证失败,无法访问系统资源,请重新登陆!");
ResponseUtil.responseJson(response, HttpStatus.UNAUTHORIZED.value(), info);
}
};
}
/**
* 退出处理
*
* @return
*/
@Bean
public LogoutSuccessHandler logoutSussHandler() {
return new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
LoginUser loginUser = tokenService.getLoginUser(request);
if (loginUser != null) {
// 删除用户缓存记录
tokenService.delLoginUser(loginUser.getToken());
}
ServerResponse info = new ServerResponse(HttpStatus.OK.value(), "退出成功");
ResponseUtil.responseJson(response, HttpStatus.OK.value(), info);
}
};
}
}
参考:
更多推荐
所有评论(0)