上一篇:SpringCloud+Gateway+Swagger2-CSDN博客
1、前言
SpringCloud和SpringBoot项目的实现方式差不多,本文主要是SpringCloud,boot的大致也会概括,我会在需要注意的地方用橙色标注。
实现这个需求,我也是借鉴了多位优秀博主的文章,过程也遇到各种问题。本文章整理了实现需求的步骤和遇到的所有的问题。
优化中……:
- token自动续期;JWT类型token自动续期-CSDN博客
- 携带短标识给前端,jwt数据存入redis,避免token过长;
- 注解式实现接口是否需要拦截;
- 鉴权;
2、项目基础
SpringCloudAlibaba项目,已集成Nacos、Gateway、Swagger2(jdk1.8)
(Gateway、Swagger2 可以看上一篇文章)
3、需求
实现SpringSecurity登录,并生成jwt给前端,接口拦截。
4、思路
要有一个用户模块,用于管理用户信息;再搭建一个auth模块用于security登录,登录操作fegin调用户模块返回用户信息。
我没有让其他服务都去引入auth模块实现拦截,因为我引入之后不知道怎么处理统一配置过滤,接口文档访问其他服务的时候总让登录,所以我是利用网关处理的token问题。
5、操作步骤
5.1 新建工具类
架构图:
5.1.1 Redis模块
cloud-commons/cloud-common-redis
作用:统一管理版本号,后边我们会有多个模块要用到(不写业务,只引入依赖)。
实现:登录成功后将用户信息存储到Redis里,请求接口时用来检验token是否正确。
引入依赖
<dependencies>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
</dependencies>
5.1.2 Utils模块
cloud-commons/cloud-common-utils
作用:将所有自定义的工具归纳到此模块里 ,比如关于日期的处理工具,异常处理等,其他模块公用的工具类统一管理。
新建JwtUtil
作用:后续验证jwt是否正确。
import io.jsonwebtoken.*;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "mingwen";
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("sg") // 签发者
.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();
}
public static void main(String[] args) throws Exception {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
Claims claims = parseJWT(token);
System.out.println(claims);
}
/**
* 生成加密后的秘钥 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;
}
//不管是否过期,都返回claims对象 解决jwt过期超时问题
public static Claims parseJWT(String jwt){
SecretKey secretKey = generalKey();
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secretKey) // 设置标识名
.parseClaimsJws(jwt) //解析token
.getBody();
} catch (ExpiredJwtException e) {
claims = e.getClaims();
}
return claims;
}
}
5.2 新建用户系统
cloud-upms-biz
作用:用户数据相关业务接口
我使用的是MybatisPlus,具体配置不详细贴了。
引入依赖
<!-- 上一篇文章中实现的swagger模块,启动类要加注解@EnableSwagger2>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>cloud-common-swagger</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--feign 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
5.2.1 表sql
现在密码是加密的,可以先改成{noop}1234,"{noop}"表示密码没有加密,这样 Spring Security 从数据库中拿到密码后,就不会对 1234进行解密。
注意:下边5.3.4目录中SecurityConfig类中,passwordEncoder方法是给密码加密,如果你明文存储需要注释这块;如果加密,加上这个给密码加密,security也会对密码按照 BCrypt方式解密。
/**
* 加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
`email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
`phonenumber` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号',
`sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像',
`user_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
`create_by` bigint(20) NULL DEFAULT NULL COMMENT '创建人的用户id',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) NULL DEFAULT NULL COMMENT '更新人',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
`del_flag` int(11) NULL DEFAULT 0 COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'admin', '管理员', '$2a$10$AoxJFiXSAKSDwA6jyKjJfOKdFJ8ikjPUTr7xPl.IVqsxJo0rUxEz.', '0', NULL, NULL, NULL, NULL, '0', NULL, NULL, NULL, NULL, 0);
5.2.2 实体类
import lombok.*;
import java.io.Serializable;
import java.util.Date;
/**
* 用户表(User)实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SysUser implements Serializable {
/**
* 主键
*/
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
/**
* 密码
*/
private String password;
/**
* 账号状态(0正常 1停用)
*/
private String status;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phonenumber;
/**
* 用户性别(0男,1女,2未知)
*/
private String sex;
/**
* 头像
*/
private String avatar;
/**
* 用户类型(0管理员,1普通用户)
*/
private String userType;
/**
* 创建人的用户id
*/
private Long createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 删除标志(0代表未删除,1代表已删除)
*/
private String delFlag;
}
5.2.3 实现接口
Controller
import com.api.commons.result.RES;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cloud.upms.entity.SysUser;
import com.cloud.upms.mapper.UserMapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping(value = "/user")
@Api(value = "用户管理", tags = "用户管理")
public class UserController {
@Autowired
private UserMapper userMapper;
@ApiOperation(value = "修改密码", notes = "修改密码")
@GetMapping("/editPassword/{userName}/{password}")
public RES editPassword(@PathVariable(value = "userName") String userName,
@PathVariable(value = "password") String password ){
// 查询用户信息
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getUserName, userName);
SysUser user = userMapper.selectOne(queryWrapper);
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
user.setPassword(passwordEncoder.encode(password));
int i = userMapper.updateById(user);
return RES.ok(0,"success",i);
}
@ApiOperation(value = "列表", notes = "列表")
@GetMapping("/getAll")
public RES getAll(){
List<SysUser> users = userMapper.selectList(null);
return RES.ok(0,"success",users);
}
@ApiOperation(value = "查询用户信息", notes = "查询用户信息")
@GetMapping("/getInfoByUserName/{userName}")
public SysUser getInfoByUserName(@PathVariable(value = "userName") String userName){
// 查询用户信息
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getUserName, userName);
SysUser user = userMapper.selectOne(queryWrapper);
return user;
}
Mapper
@Mapper
public interface UserMapper extends BaseMapper<SysUser> {}
Feign接口
(feign调用实现过程我也不详细贴了)
提供给登录时使用
@FeignClient(contextId = "feignUpmsClient", value = "cloud-upms-biz")
public interface FeignUpmsClient {
@GetMapping("user/getInfoByUserName/{userName}")
SysUser getInfoByUserName(@PathVariable(value = "userName") String userName);
}
5.3 新建Security系统
cloud-api-auth
作用:处理登录操作
引入依赖
<!-- security-oauth2 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入其他模块的依赖 -->
<!-- swagger -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>cloud-common-swagger</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- 用户模块的-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>cloud-upms-biz</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- redis-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>cloud-common-redis</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- utils-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>cloud-common-utils</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
5.3.1 实体类
/**
* userDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,
* 把用户信息封装在其中.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private SysUser 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;
}
}
5.3.2 Redis工具类
(yml自行配置,不具体贴了)
package com.cloud.auth.config;
import com.cloud.auth.utils.FastJsonRedisSerializer;
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
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;
}
}
package com.cloud.auth.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
/**
* 序列化
* @param <T>
*/
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);
}
}
package com.cloud.auth.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;
/**
* 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 缓存键值
* @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);
}
}
5.3.3 实现接口
(登录,登出,基于security)
package com.cloud.auth.controller;
import com.api.commons.result.RES;
import com.cloud.auth.service.impl.LoginService;
import com.cloud.upms.entity.SysUser;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@Api(value = "授权管理", tags = "授权管理")
@RequestMapping("auth")
public class AuthController {
@Autowired
private LoginService loginService;
@ApiOperation(value = "登录", notes = "登录")
@PostMapping("/login")
public RES login(@RequestBody SysUser sysUser){
return loginService.login(sysUser);
}
@ApiOperation(value = "退出", notes = "退出")
@GetMapping("/logout")
public RES logout() {
String msg = loginService.logout();
return RES.ok(0,"success", "注销成功");
}
}
package com.cloud.auth.service.impl;
import com.api.commons.result.RES;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cloud.upms.entity.SysUser;
public interface LoginService extends IService<SysUser> {
RES login(SysUser user);
String logout();
}
package com.cloud.auth.service;
import com.api.commons.jwt.JwtUtil;
import com.api.commons.result.RES;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cloud.auth.domain.LoginUser;
import com.cloud.auth.mapper.LoginMapper;
import com.cloud.auth.service.impl.LoginService;
import com.cloud.auth.utils.RedisCache;
import com.cloud.upms.entity.SysUser;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 登录相关逻辑
*/
@Service
public class LoginServiceImpl extends ServiceImpl<LoginMapper, SysUser> implements LoginService {
@Resource
private AuthenticationManager authenticationManager;
@Resource
public RedisCache redisCache;
@Override
public RES login(SysUser user) {
// 认证的时候需要Authentication对象,所以需要一个Authentication的实现类,这里选择了UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
// AuthenticationManager authenticate方法进行认证。在SecurityConfig配置类中,我们将AuthenticationManager注入到容器中。
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 如果认证通过,authenticate里将包含principal属性,该属性的值就是LoginUser,
// 如果认证没通过,给出对应的提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败");
}
// 如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回
//1.1.1 可优化携带短标识给前端,jwt数据存入redis,避免因token太长而占用太多的带宽
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String id = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(id);
Map<String, String> map = new HashMap<>();
map.put("token", jwt);
// 把完整的用户信息存入redis,userid作为key
redisCache.setCacheObject("login:" + id, loginUser);
return new RES(200, "登陆成功", map);
}
@Override
public String logout() {
// 获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication =
(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long id = loginUser.getUser().getId();
// 删除redis当中的值
redisCache.deleteObject("login:" + id);
return "注销成功";
}
}
5.3.4 SecurityConfig
package com.cloud.auth.config;
import com.cloud.auth.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @Description: SpringSecurity的基本配置
**/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/auth/login").anonymous()
.antMatchers("/auth/logout").anonymous()
.antMatchers("/doc.html",
"/v2/**",//此请求不放开会导致 error api-docs无法正常显示 https://songzixian.com/javalog/905.html
"/swagger-ui**",
"/swagger-ui/**",//此请求不放开没有权限请求一直失败,处于轮询接口
"/swagger-resources/**",//此请求不放开导致访问出现Unable to infer base url. This is common when using dynamic servlet registration or when the API is https://blog.csdn.net/just_now_and_future/article/details/89343680
"/webjars/**"
).permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 在过滤器UsernamePasswordAuthenticationFilter之前,添加我们自定义的过滤器JwtAuthenticationTokenFilter
// http.addFilterBefore(jwtAuthenticationTokenFilter,
// UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
第59-61行,如果我们是SpringBoot项目可以放开,下边是他的过滤器配置,它会去拦截我们的接口传入的token。
package com.cloud.auth.filter;
import com.api.commons.jwt.JwtUtil;
import com.cloud.auth.domain.LoginUser;
import com.cloud.auth.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
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 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;
/**
* 过滤器,校验token(不是全局的 可以单体项目使用,用最新代码gateway下的)
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String contextPath = request.getRequestURI(); //获取url
String token = request.getHeader("token"); // 获取token
if (contextPath.equals("/auth/login")){ //登录接口不验证jwt
// 放行,后面还有其他过滤器
filterChain.doFilter(request, response);
// 所有过滤器执行完毕后,响应回来还会走到这里
return;
}
if (!StringUtils.hasText(token)) {
// 放行,后面还有其他过滤器
filterChain.doFilter(request, response);
// 所有过滤器执行完毕后,响应回来还会走到这里
return;
}
// 解析token
String id;
try {
Claims claims = JwtUtil.parseJWT(token);
id = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
// 从redis中获取用户信息
String redisKey = "login:" + id;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
// 后面将需要一个Authentication的对象,在这里通过实现类UsernamePasswordAuthenticationToken构造这个对象
// 选择3个参数的构造器,principal:账号,credentials:密码,authorities:权限
// 为什么要选择这个构造器呢?因为这个构造器中有,super.setAuthenticated(true); 标识用户为已认证。
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
// 存入SecurityContextHolder
// 存入需要一个Authentication的对象,在登录的时候也用到过类似的方法。
// TODO 获取权限信息封装到Authentication
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}
5.4 修改网关模块-过滤器
cloud-api-gateway
用户每次请求接口验证token问题,在cloud-api-auth下判断不是全局的,我们有网关,所有接口首先经过网关,所以我再 cloud-api-gateway模块下增加过滤器验证token。
package com.cloud.gateway.filter;
import cn.hutool.json.JSONObject;
import com.api.commons.exception.ExceptionEnum;
import com.api.commons.jwt.JwtUtil;
import com.cloud.gateway.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Objects;
/**
* 全局过滤器校验jwt token
*/
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Autowired
private RedisCache redisCache;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取请求对象和响应对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//1.1.处理返回结果
JSONObject message = new JSONObject();
//ExceptionEnum是我自定义的枚举类,你们可以自定义code和value
message.put("code", ExceptionEnum.SIGNATURE_NOT_MATCH.getResultCode());
message.put("msg", ExceptionEnum.SIGNATURE_NOT_MATCH.getResultMsg());
byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
//2.判断当前的请求是否为登录,如果是,直接放行
if(request.getURI().getPath().contains("/auth/login")||
request.getURI().getPath().contains("/v2/api-docs")){
//放行
return chain.filter(exchange);
}
//3.获取当前用户的请求头jwt信息
HttpHeaders headers = request.getHeaders();
String jwtToken = headers.getFirst("token");
//4.判断当前令牌是否存在
if(StringUtils.isEmpty(jwtToken)){
//如果不存在,向客户端返回错误提示信息
System.out.println("----未传token---");
return response.writeWith(Mono.just(buffer));
}
//5.如果令牌存在,解析jwt令牌,判断该令牌是否合法,如果不合法,则向客户端返回错误信息
String id;
try {
Claims claims = JwtUtil.parseJWT(jwtToken);
Date expiration = claims.getExpiration(); //解决token过期
//和当前时间进行对比来判断是否过期
boolean after = new Date(System.currentTimeMillis()).after(expiration);
if (after){
//token过期
System.out.println("-----token过期----");
return response.writeWith(Mono.just(buffer));
}
id = claims.getSubject();
// 从redis中获取用户信息
String redisKey = "login:"+id;
Object loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)) {
System.out.println("-----token错误----");
return response.writeWith(Mono.just(buffer));
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("-----异常----");
return response.writeWith(Mono.just(buffer));
}
//6.放行
return chain.filter(exchange);
}
/**
* 优先级设置
* 值越小,优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
5.5 结果展示
成功
token异常
6、遇到的问题
6.1 过滤器不是全局的导致接口不拦截
项目集成了gateway,接口都要从网关经过,所以在网关模块下增加了过滤器(目录5.4的方法),校验token。
6.2 jwt超时过期报错问题
目录5.4 的AuthorizeFilter 方法 75-84 行。
6.3 异常过滤器往前端抛出返回问题。
原来直接 throw new RuntimeException("用户未登录");return; 每次异常只在后端抛出,不在前端展示。
修改成了 return response.writeWith(Mono.just(buffer));
6.4 swagger 接口文档被拦截问题
在过滤器上放行文档地址。
6.5 redis取值序列化问题
存储对象时jdk会自动序列化存储,看着像是乱码。我们 自定义序列化,存和取得序列化格式要一致,不然取不到。
作为个人学习笔记并分享,如果有问题欢迎指正~~~