SpringSecurity整合JWT实现认证授权

本篇文章主要是 springboot 整合 jwt 实现用户的登录认证与授权,并且还有单用户共享 token、单设备登录、多设备登录、同端互斥登录与临时 token 等。git 地址:点击前往
阅读需要熟悉 spring security 与 jwt 相关知识,也可前往链接学习:
Spring Security入门
十分钟学会JWT

一、准备阶段

1、表

1.用户表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `account` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '账号',
  `user_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
  `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户密码',
  `last_login_time` datetime(0) NULL DEFAULT NULL COMMENT '上一次登录时间',
  `enabled` tinyint(1) NULL DEFAULT 1 COMMENT '账号是否可用。默认为1(可用)',
  `account_not_expired` tinyint(1) NULL DEFAULT 1 COMMENT '是否过期。默认为1(没有过期)',
  `account_not_locked` tinyint(1) NULL DEFAULT 1 COMMENT '账号是否锁定。默认为1(没有锁定)',
  `credentials_not_expired` tinyint(1) NULL DEFAULT 1 COMMENT '证书(密码)是否过期。默认为1(没有过期)',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
  `deleted` tinyint(1) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'zhangsan', '张三', '$2a$10$47lsFAUlWixWG17Ca3M/r.EPJVIb7Tv26ZaxhzqN65nXVcAhHQM4i', '2019-09-04 20:25:36', 1, 1, 1, 1, '2019-08-29 06:28:36', '2019-09-04 20:25:36', 0);
INSERT INTO `sys_user` VALUES (2, 'lisi', '李四', '$2a$10$uSLAeON6HWrPbPCtyqPRj.hvZfeM.tiVDZm24/gRqm4opVze1cVvC', '2019-09-05 00:07:12', 1, 1, 1, 1, '2019-08-29 06:29:24', '2019-09-05 00:07:12', 0);

SET FOREIGN_KEY_CHECKS = 1;

2.角色表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `role_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色代码',
  `role_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色名',
  `role_description` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色说明',
  `create_time` datetime(0) NULL DEFAULT NULL,
  `update_time` datetime(0) NULL DEFAULT NULL,
  `deleted` tinyint(1) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户角色表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'admin', '管理员', '管理员,拥有所有权限', '2023-12-11 10:15:29', NULL, 0);
INSERT INTO `sys_role` VALUES (2, 'user', '普通用户', '普通用户,拥有部分权限', '2023-12-11 10:15:31', NULL, 0);

SET FOREIGN_KEY_CHECKS = 1;

3.权限表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `permission_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限code',
  `permission_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限名',
  `create_time` datetime(0) NULL DEFAULT NULL,
  `update_time` datetime(0) NULL DEFAULT NULL,
  `deleted` tinyint(1) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '权限表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, 'create_user', '创建用户', '2023-12-11 10:17:23', NULL, 0);
INSERT INTO `sys_permission` VALUES (2, 'query_user', '查看用户', '2023-12-11 10:17:25', NULL, 0);
INSERT INTO `sys_permission` VALUES (3, 'delete_user', '删除用户', '2023-12-11 10:17:27', NULL, 0);
INSERT INTO `sys_permission` VALUES (4, 'modify_user', '修改用户', '2023-12-11 10:17:30', NULL, 0);

SET FOREIGN_KEY_CHECKS = 1;

4.用户角色关系表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_user_role_relation
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role_relation`;
CREATE TABLE `sys_user_role_relation`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `user_id` int(11) NULL DEFAULT NULL COMMENT '用户id',
  `role_id` int(11) NULL DEFAULT NULL COMMENT '角色id',
  `create_time` datetime(0) NULL DEFAULT NULL,
  `update_time` datetime(0) NULL DEFAULT NULL,
  `deleted` tinyint(1) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户角色关联关系表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user_role_relation
-- ----------------------------
INSERT INTO `sys_user_role_relation` VALUES (1, 1, 1, '2023-12-11 10:18:23', NULL, 0);
INSERT INTO `sys_user_role_relation` VALUES (2, 2, 2, '2023-12-11 10:18:26', NULL, 0);

SET FOREIGN_KEY_CHECKS = 1;

5.角色权限关系表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_role_permission_relation
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission_relation`;
CREATE TABLE `sys_role_permission_relation`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `role_id` int(11) NULL DEFAULT NULL COMMENT '角色id',
  `permission_id` int(11) NULL DEFAULT NULL COMMENT '权限id',
  `create_time` datetime(0) NULL DEFAULT NULL,
  `update_time` datetime(0) NULL DEFAULT NULL,
  `deleted` tinyint(1) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色-权限关联关系表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role_permission_relation
-- ----------------------------
INSERT INTO `sys_role_permission_relation` VALUES (1, 1, 1, '2023-12-11 10:19:19', NULL, 0);
INSERT INTO `sys_role_permission_relation` VALUES (2, 1, 2, '2023-12-11 10:19:21', NULL, 0);
INSERT INTO `sys_role_permission_relation` VALUES (3, 1, 3, '2023-12-11 10:19:23', NULL, 0);
INSERT INTO `sys_role_permission_relation` VALUES (4, 1, 4, '2023-12-11 10:19:25', NULL, 0);
INSERT INTO `sys_role_permission_relation` VALUES (5, 2, 1, '2023-12-11 10:19:29', NULL, 0);
INSERT INTO `sys_role_permission_relation` VALUES (6, 2, 2, '2023-12-11 10:19:31', NULL, 0);

SET FOREIGN_KEY_CHECKS = 1;

对应的实体类可前往我的 git 的 pojo.entity 包 里查看,这里因篇幅原因就不贴出来了

2、项目框架

1.maven 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.2</version>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.18</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency>
<!-- 这两个依赖后续再引入 -->
<!-- <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency> -->

2.配置

spring:
  application:
    name: security_jwt_demo
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security_jwt_demo?serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  redis:
    host: localhost
    port: 6379
    database: 3

server:
  port: 8080
  servlet:
    context-path: /v1/api

mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml
  typeAliasesPackage: club.gggd.security_jwt_demo.pojo
  global-config:
    id-type: 0
    field-strategy: 1
    db-column-underline: true
    refresh-mapper: true
    logic-delete-value: 0
    logic-not-delete-value: 1
    sql-parser-cache: true
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false

jwt:
  #JWT存储的请求头
  requestHeader: Authorization
  #JWT加解密使用的密钥
  secret: symx.club
  #JWT的有效时间(60*60*24*7)
  expiration: 604800
  #JWT负载中的开头
  tokenStartWith: 'Bearer '

3、其他类

1.跨域配置类

@Configuration(proxyBeanMethods = false)
@EnableWebMvc
public class ConfigurerAdapter implements WebMvcConfigurer {
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        // 允许cookies跨域
        config.setAllowCredentials(true);
        // #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
        config.addAllowedOriginPattern("*");
        // #允许访问的头信息,*表示全部
        config.addAllowedHeader("*");
        // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
        config.setMaxAge(18000L);
        // 允许提交请求的方法类型,*表示全部允许
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 静态资源过滤,需同时在WebSecurityConfig放行
    }
}

2.Redis 序列化配置

@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
        //使用 <String, Object>类型
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        //序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        //String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //key 采用String 的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash 的key 也采用String 的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value 序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash 的value序列化方式采用Jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

3.实体类基类

@Data
public class BaseEntity {
    @TableField("create_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;

    @TableField("update_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;

    @TableField("deleted")
    @TableLogic
    private Integer deleted;
}

4.自定义异常

@Data
public class BusinessException extends RuntimeException{

    private int code;

    private String message;

    public BusinessException(){}

    public BusinessException(ResultCode resultCode) {
        this.code = resultCode.getCode();
        this.message = resultCode.getMessage();
    }

    public BusinessException(int code, String mgs) {
        this.code = code;
        this.message = mgs;
    }

    public BusinessException(String mgs) {
        this.code = 400;
        this.message = mgs;
    }
}
@Data
public class TokenException extends RuntimeException{

    public TokenException(String message) {
        super(message);
    }

    public TokenException(ResultCode resultCode) {
        super(resultCode.getMessage());
    }
}

5.返回状态码

@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum ResultCode {

    SUCCESS(200, "成功"),

    BAD_REQUEST(400, "请求错误"),

    UNAUTHORIZED(401, "用户未认证,请登录"),

    FORBIDDEN(403, "禁止访问"),

    NOT_FOUND(404, "内容未找到"),

    METHOD_NOT_ALLOW(405, "不支持的方法"),

    SERVER_ERROR(500, "服务器内部发生错误"),

    /* 参数错误 */
    PARAM_NOT_VALID(1001, "参数无效"),
    PARAM_IS_BLANK(1002, "参数为空"),
    PARAM_TYPE_ERROR(1003, "参数类型错误"),
    PARAM_NOT_COMPLETE(1004, "参数缺失"),

    /* 用户错误 */
    USER_NOT_LOGIN(2001, "用户未登录"),
    USER_ACCOUNT_EXPIRED(2002, "账号已过期"),
    USER_CREDENTIALS_ERROR(2003, "密码错误"),
    USER_CREDENTIALS_EXPIRED(2004, "密码过期"),
    USER_ACCOUNT_DISABLE(2005, "账号不可用"),
    USER_ACCOUNT_LOCKED(2006, "账号被锁定"),
    USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),
    USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),
    USER_ACCOUNT_USE_BY_OTHERS(2009, "账号下线"),
    USER_NOT_FOUND(2010, "用户不存在"),

    TOKEN_INVALID(3001, "token无效"),
    TOKEN_TIMEOUT(3002, "token已过期"),

    CAPTCHA_ERR(4001, "验证码错误");

    /**
     * 返回码
     */
    private Integer code;

    /**
     * 返回信息
     */
    private String message;
}

6.统一返回体

@Data
@NoArgsConstructor
@AllArgsConstructor
public class WebResult<T> {

    private Boolean success;

    private Integer code;

    private String message;

    private T data;

    public static WebResult success() {
        WebResult result = new WebResult();
        result.success = true;
        result.code = 200;
        result.message = "成功";
        return result;
    }

    public static WebResult success(String message) {
        WebResult result = new WebResult();
        result.success = true;
        result.code = 200;
        result.message = message;
        return result;
    }

    public static WebResult success(Object data) {
        WebResult result = new WebResult();
        result.success = true;
        result.code = 200;
        result.message = "成功";
        result.data = data;
        return result;
    }

    public static WebResult error() {
        WebResult result = new WebResult();
        result.success = false;
        result.code = 400;
        result.message = "失败";
        return result;
    }

    public static WebResult error(String message) {
        WebResult result = new WebResult();
        result.success = false;
        result.code = 400;
        result.message = message;
        return result;
    }

    public static WebResult error(int code, String message) {
        WebResult result = new WebResult();
        result.success = false;
        result.code = code;
        result.message = message;
        return result;
    }

    public static WebResult error(ResultCode resultCode) {
        WebResult result = new WebResult();
        result.success = false;
        result.code = resultCode.getCode();
        result.message = resultCode.getMessage();
        return result;
    }
}

7.全局异常捕获

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandle {

    /**
     * 拦截业务异常
     * @param e
     * @param request
     * @param response
     * @return
     */
    @ResponseBody
    @ExceptionHandler(BusinessException.class)
    public WebResult handlerBusinessException(Exception e, HttpServletRequest request, HttpServletResponse response) {
        log.error("业务异常信息:{}", e.getMessage());
        e.printStackTrace();
        // 不同异常返回不同状态码
        WebResult result = WebResult.error(((BusinessException) e).getCode(), e.getMessage());
        return result;
    }

    @ResponseBody
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public WebResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request, HttpServletResponse response) {
        log.error("请求参数异常:{}", e.getMessage());
        List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
        // 不同异常返回不同状态码
        WebResult result = WebResult.error(ResultCode.BAD_REQUEST.getCode(), allErrors.get(0).getDefaultMessage());
        return result;
    }

    /**
     * 处理其他异常
     * @param e
     * @param request
     * @param response
     * @return
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public WebResult handleException(Exception e, HttpServletRequest request, HttpServletResponse response){
        log.error("根异常信息:{}", e.getMessage());
        e.printStackTrace();
        // 不同异常返回不同状态码
        WebResult result = WebResult.error(ResultCode.SERVER_ERROR.getCode(), ResultCode.SERVER_ERROR.getMessage());
        return result;
    }
}

8.Redis 工具类

网上有很多,也可以用我的:Redis工具类

二、开始整合

首先把上面 maven 依赖中的 spring security 和 jwt 依赖导入。
然后写一个 jwt 工具类:

@Data
@Component
// 引入配置类中jwt相关配置
@ConfigurationProperties("jwt")
public class JwtUtil {

    private String requestHeader;
    private String secret;
    private int expiration;
    private String tokenStartWith;

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 获取用户名
     * @return
     */
    public String getAccountByToken(String token) {
        Claims claims = getClaimsByToken(token);
        return claims != null ? claims.getSubject() : null;
    }

    /**
     * 获取过期时间
     * @param token
     * @return Date
     */
    public Date getExpiredByToken(String token) {
        Claims claims = getClaimsByToken(token);
        return claims != null ? claims.getExpiration() : null;
    }

    /**
     * 获取Claims
     * @param token
     * @return
     */
    private Claims getClaimsByToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            // 签名不一致异常
            if (e instanceof SignatureException) {
                throw new TokenException(ResultCode.TOKEN_INVALID);
            }
            // token过期异常
            if (e instanceof ExpiredJwtException) {
                throw new TokenException(ResultCode.TOKEN_TIMEOUT);
            }
            // 如果都不是上面的则弹出token无效异常
            throw new TokenException(ResultCode.TOKEN_INVALID);
        }
        return claims;
    }

    /**
     * 计算过期时间
     * @return
     */
    private Date generateExpired() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 判断 Token 是否过期
     * @param token
     * @return
     */
    private Boolean isTokenExpired(String token) {
        Date expirationDate = getExpiredByToken(token);
        return expirationDate.before(new Date());
    }

    /**
     * 生成 Token
     * @param user 用户信息
     * @return
     */
    public String generateToken(SysUser user) {
        String token = Jwts.builder()
                .setSubject(user.getAccount())
                .setExpiration(generateExpired())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
        String key = "login:" + user.getAccount() + ":" + token;
        redisUtil.set(key, token, expiration);
        return token;
    }

    /**
     * 验证 Token
     * @param token
     * @return
     */
    public Boolean validateToken(String token) {
        String account = getAccountByToken(token);
        String key = "login:" + account+ ":" + token;
        Object data = redisUtil.get(key);
        String redisToken = data == null ? null : data.toString();
        return StrUtil.isNotEmpty(token) && !isTokenExpired(token) && token.equals(redisToken);
    }

    /**
     * 移除 Token
     * @param token
     */
    public void removeToken(String token) {
        String account = getAccountByToken(token);
        String key = "login:" + account+ ":" + token;
        redisUtil.del(key);
        delUserDetail(account);
    }

    /**
     * 获取token
     * @param request
     * @return
     */
    public String getToken(HttpServletRequest request) {
        String auth = request.getHeader(requestHeader);
        if (StrUtil.isBlank(auth)) {
            return null;
        }
        String token = auth.replace(tokenStartWith, "");
        return token;
    }

    /**
     * 退出登录
     */
    public void logout(HttpServletRequest request) {
        String token = getToken(request);
        removeToken(token);
    }

    /**
     * 获取userDetail
     * @param token
     * @return
     */
    public JwtUser getUserDetail(String token) {
        String account = getAccountByToken(token);
        String s = (String) redisUtil.get("user:userDetail:" + account);
        JwtUser jwtUser = JSON.parseObject(s, JwtUser.class);
        return jwtUser;
    }

    /**
     * 删除userDetail
     * @param account
     */
    public void delUserDetail(String account) {
        redisUtil.del("user:userDetail:" + account);
    }
}

1、登录接口

首先用户需要进行登录,然后后台生成一个 token 返回给前端,之后前端就需要在请求头带上这个 token,否则会根据 Spring Security 的 配置判断用户是否可以访问这个接口。
在登录前需要先实现 UserDetailsService 接口,这个接口是登录的重要部分,用于构造并保存登录用户信息,而 UserDetails 接口则是用来记录用户信息的类,我们需要写一个类来实现 UserDetails 接口,这个类主要是一些用户的基本信息,以及权限等。

@Getter
@AllArgsConstructor
public class JwtUser implements UserDetails {

    private final Integer id;

    private final String account;

    private final String userName;

    @JsonIgnore
    private final String password;

    @JsonIgnore
    private final Collection<SimpleGrantedAuthority> authorities;

    private final boolean enabled;

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return account;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public Collection getRoles() {
        return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
    }
}

UserDetails 写完了,接下来就是 UserDetailsService 的实现了,实现这个接口,在登录时会调用这个接口,将用户信息存到 Redis 中。userService.getAuthList(user.getId())方法后面有说

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
        // 获取用户信息
        UserService userService = (UserService) applicationContext.getBean("userServiceImpl");
        SysUser user = userService.getUserInfo(account);
        if (user == null) {
            throw new BusinessException(ResultCode.USER_NOT_FOUND);
        }
        // 组装成userDetails
        JwtUser jwtUser = new JwtUser(
                user.getId(),
                user.getAccount(),
                user.getUserName(),
                user.getPassword(),
                userService.getAuthList(user.getId()),
                user.getEnabled() == 1 ? true : false
        );
        // 存入Redis
        String s = JSON.toJSONString(jwtUser);
        redisUtil.set("user:userDetail:" + user.getAccount(), s);
        return jwtUser;
    }
}

接下来就是登录接口了,下面是一个简单的登录接口:

@PostMapping("/login")
public WebResult login(@RequestBody @Validated LoginVo vo) {
    // 解密密码,所有的密码都需要进行一个加密,不能明文传输,我这里就不写了
    
    // 判断验证码,写死,不要学
    if (!"1234".equals(vo.getCode())) {
        return WebResult.error("验证码错误");
    }
    String token = userService.login(vo);
    return WebResult.success((Object) token);
}

login 方法:

public String login(LoginVo vo) {
    // 判断用户
    LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(SysUser::getAccount, vo.getAccount());
    SysUser user = userMapper.selectOne(wrapper);
    Optional.ofNullable(user).orElseThrow(() -> new BusinessException(ResultCode.USER_NOT_FOUND));
    boolean matches = passwordEncoder.matches(vo.getPassword(), user.getPassword());
    if (!matches) {
        throw new BusinessException(ResultCode.USER_CREDENTIALS_ERROR);
    }
    // 组装Authentication,在这里会调用到UserDetailsService的loadUserByUsername方法
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(vo.getAccount(), vo.getPassword());
    Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
    SecurityContextHolder.getContext().setAuthentication(authentication);
	// 生成token
    String token = jwtUtil.generateToken(user);
    return token;
}

用户进行登录后,后台就会判断验证码,密码是否正确,然后通过 Spring Security 的loadUserByUsername 方法保存用户信息,这样我们就可以通过 token 解析到用户账号,然后拿到用户信息,就不需要实时查询数据库了。

2、jwt 过滤器

用户登录后,后台会生成一个 token 返回给前端,至此前端每次进行请求都需要带上这个 token,这样后台才能确认到究竟是哪个用户进行访问。而接下来就是要进行 token 的解析了,通过解析 token,我们可以验证 token 是否正确和构造出当前请求的用户,然后将其保存到 Spring Security 的SecurityContext 中去。

@Slf4j
public class JwtFilter extends BasicAuthenticationFilter{

    private final JwtUtil jwtUtil;

    public JwtFilter(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
        super(authenticationManager);
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 获取token
        String token = jwtUtil.getToken(request);
        // 如果token为空,则进入下一个过滤器,因为有些接口是允许匿名访问的
        if (token == null) {
            chain.doFilter(request, response);
            return;
        }

        String account = jwtUtil.getAccountByToken(token);
        if (account != null && SecurityContextHolder.getContext().getAuthentication() == null && jwtUtil.validateToken(token)) {
            // 将用户信息存入SecurityContext
            UserDetails userDetails = jwtUtil.getUserDetail(token);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } else {
            jwtUtil.removeToken(token);
        }
        chain.doFilter(request, response);
    }
}

3、认证失败过滤器

当用户带上 token 请求某个接口时,会经过 jwt 过滤器对 token 进行一个验证,假如验证不通过,例如有人篡改了 token 或者 token 过期了,就会执行认证失败过滤器,这个过滤器的作用主要是提示前端该用户登录过期了需要重新登录,需要实现接口AuthenticationEntryPoint。

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应
        WebResult result = WebResult.error(ResultCode.UNAUTHORIZED);
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(result));
    }
}

4、无权限访问过滤器

有些接口可能需要某些特定的权限才能访问,这时候如果用户没有这个权限但仍然访问了这个接口,这时候我们就需要进行一个拦截并且返回提示消息,就需要到了无权限访问过滤器了,这个过滤器同样需要实现一个接口AccessDeniedHandler

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        //当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
        WebResult result = WebResult.error(ResultCode.FORBIDDEN);
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(result));
    }
}

需要注意的是,如果要实现接口的权限控制,需要在 Spring Security 的配置类中加上注解@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true),这个注解允许接口级别的权限控制,稍后我会贴出完整的配置类,这里稍作了解即可。加上注解后,我们就可以在接口方法上加上@PreAuthorize注解进行接口级的权限判断了。
一般的项目中都会有一个超级管理员角色,这个角色掌握所有的权限,如果仅通过 Spring Security 自带的注解,那么每个注解上都需要加上超级管理员,这样也是非常麻烦的,所以我们可以自己写一个验证权限的类,在里面把管理员给放行。

@Service(value = "el")
public class ElPermissionHandle {

    @Autowired
    private UserService userService;

    @Autowired
    private UserProvider userProvider;

    public Boolean check(String ...permissions){
        // 获取当前用户的所有权限
        Integer userId = userProvider.getUserId();
        List<String> list = userService.getAuthList(userId).stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        return list.contains("admin") || Arrays.stream(permissions).anyMatch(list::contains);
    }

}

获取用户权限的方法userService.getAuthList(userId)

public List<SimpleGrantedAuthority> getAuthList(Integer userId) {
    // 获取该用户所有权限
    List<UserPermission> list = userMapper.getUserPermissionList(userId);
    Set<String> permission = list.stream().map(UserPermission::getPermissionCode).collect(Collectors.toSet());
    // 获取该用户所有角色
    List<UserRoleDto> userRoleList = roleMapper.getUserRoleList(userId);
    for (UserRoleDto ur : userRoleList) {
        permission.add(ur.getRoleCode());
    }
    List<SimpleGrantedAuthority> authorities = permission.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    return authorities;
}

以及 SQL:

<select id="getUserPermissionList" parameterType="integer" resultType="UserPermission">
  SELECT
    u.id userId,
    u.account,
    p.permission_code permissionCode
  FROM
  	sys_user u
    INNER JOIN sys_user_role_relation ur ON ur.user_id = u.id AND ur.deleted = 0
    INNER JOIN sys_role_permission_relation rp ON rp.role_id = ur.role_id AND rp.deleted = 0
    INNER JOIN sys_permission p ON p.id = rp.permission_id AND p.deleted = 0
  WHERE
    u.deleted = 0
    AND u.id = #{userId}
</select>

<select id="getUserRoleList" parameterType="integer" resultType="UserRoleDto">
    SELECT
        ur.user_id userId,
        r.role_code roleCode
    FROM
        sys_user_role_relation ur
        INNER JOIN sys_role r ON r.id = ur.role_id AND r.deleted = 0
    WHERE
        ur.deleted = 0
      	AND ur.user_id = #{userId}
</select>

使用的话如下例子:

@GetMapping("/security")
@PreAuthorize("@el.check('create_user')")
public WebResult getSecurityInfo() {
    return WebResult.success("获取成功");
}

最后还有一点要注意的,这也是我做的时候遇到的坑,就是一切都配置好了之后发现没有权限时直接抛出了异常AccessDeniedException,而没有走我配置的过滤器,后面才发现原来是被全局异常捕获给捕获到了,所以我们需要在GlobalExceptionHandle 这个类中先捕获到AccessDeniedException 异常,再把异常继续抛出去让无权限访问过滤器给处理掉。

/**
 * 解决被全局异常捕获的问题
 * @param e
 * @throws AccessDeniedException
 */
@ExceptionHandler(AccessDeniedException.class)
public void accessDeniedException(AccessDeniedException e) throws AccessDeniedException {
    throw e;
}

5、用户工具类

很多情况下我们都只需要到用户 id 或者用户账号,所以可以写一个工具类来获取当前请求的用户信息。

@Component
public class UserProvider {

    /**
     * 获取当前用户信息
     * @return
     */
    public JwtUser curUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        JwtUser jwtUser = (JwtUser) authentication.getPrincipal();
        return jwtUser;
    }

    /**
     * 获取用户id
     * @return
     */
    public Integer getUserId() {
        return curUser().getId();
    }

    /**
     * 获取用户账号
     * @return
     */
    public String getAccount() {
        return curUser().getAccount();
    }
}

6、整合配置

最后将上面的配置都整合到 Spring Security 配置类中

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
// 加上该注解可实现接口级的权限认证
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private JwtAccessDeniedHandler jwtAccessDeniedHandler;

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    private CorsFilter corsFilter;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //配置认证方式
        auth.userDetailsService(userDetailsService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //http相关的配置,包括登入登出、异常处理、会话管理等
        http.csrf().disable()
            // 跨域过滤器
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
            // jwt过滤器
            .addFilter(jwtFilter())
            // 授权异常
            .exceptionHandling()
            .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            .accessDeniedHandler(jwtAccessDeniedHandler)
            // 防止iframe 造成跨域
            .and()
            .headers()
            .frameOptions()
            .disable()
            // 不创建会话
            .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            // 放行与拦截接口
            .and().authorizeRequests()
            // 放行登录接口
            .antMatchers("/auth/login").permitAll()
            // 拦截其余接口
            .anyRequest().authenticated();
    }

    /**
     * 设置密码加密方式,密码必须要加密保存
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * token过滤器
     * @return
     */
    private JwtFilter jwtFilter() throws Exception{
        return new JwtFilter(authenticationManager(), jwtUtil);
    }

    /**
     * 去除 ROLE_ 前缀
     * @return
     */
    @Bean
    GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults("");
    }

}

到这里就大致整合完毕了,大家可以自己测试一下效果。

三、其他功能的实现

上面只是整合了 Spring Security 和 JWT,已经可以实现简单的登录了,但是实际上可能业务没那么简单,而是需要不同的登录功能,比如有些项目只允许一个设备端登录,在某个设备登录后之前登录的设备就要下线了,我这里也做了几个简单的登录类型:单设备登录,多设备登录和同端互斥登录。

  • 单设备登录:只允许一个设备登录,后登录的会把前登录的给踢掉
  • 多设备登录:允许多个设备同时登录,不做限制
  • 同端互斥登录:同一种设备类型只允许一次登录,类似 QQ,电脑和安卓可以同时登录,但是不允许两台电脑同时登录

在开始之前,我们需要确认一下要如何保存 token 信息,我们需要把 token 信息保存在 redis,需要通过 redis 来确认用户是否登录,下面是我设计的一种 token 保存方式。

  • session 信息:这里主要是保存这个账号下的所有 token 的,key 为 login:session:account
  • token 信息:这里是保存这个 token 所关联的账号,key 为 login:token:token

两者结构如下:

在这里插入图片描述


session 信息的结构,主要是保存了这个账号下的所有 token ,以及登录的设备和 token 到期时间:

在这里插入图片描述

token 信息的结构:

在这里插入图片描述


通过这个结构,我们可以根据账号来获取到这个账号下的所有 token,也可以仅根据 token 拿到这个 token 所对应的账号。接下来所谓的各种登录方式也就是将这些 token 按规定删除就可以实现各种类型的登录了。

1、准备

1.配置文件

login:
  # 是否共享token,仅当login-model为multi时有效
  is-share: false
  # 登录模式,分为alone,multi和same-device-exclusion三种
  login-model: alone

2.登录模式枚举

@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum LoginModel {

    ALONE("alone", "单设备登录"),
    MULTI("multi", "多设备登录"),
    SAME_DEVICE_EXCLUSION("same-device-exclusion", "同端互斥登录");

    private String value;
    private String desc;

}

3.设备类型枚举

@Getter
@AllArgsConstructor
public enum DeviceType {

    DEFAULT("DEFAULT", "默认设备"),
    ANDROID("ANDROID", "安卓设备"),
    IOS("IOS", "苹果设备"),
    PC("PC", "Windows设备");

    private String value;

    private String desc;

}

4.token 常量

// 这个是保存到redis里key的前缀
@Data
public class TokenConstant {

    public static final String TOKEN = "login:token:";

    public static final String SESSION = "login:session:";

}

5.token 信息

@Data
public class TokenInfo {

    /**
     * key的值
     */
    private String id;

    /**
     * 创建时间
     */
    private Long createTime;

    /**
     * token集合
     */
    private List<TokenSign> tokenSignList;

}
@Data
public class TokenSign {

    /**
     * token
     */
    private String token;

    /**
     * 设备
     */
    private String device;

    /**
     * 有效截止期
     */
    private Long deadline;

}

6.UserAgent 工具类

public class UserAgentUtil {

    /**
     * 获取当前请求的User-Agent
     * @return 
     */
    private static String getUserAgent() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String header = request.getHeader("User-Agent");
            return header;
        }
        return null;
    }

    /**
     * 获取当前请求的设备
     * @return
     */
    public static String getDevice() {
        String userAgent = getUserAgent();
        if (userAgent == null) {
            return DeviceType.DEFAULT.getValue();
        }
        if (userAgent.toLowerCase().contains(DeviceType.ANDROID.getValue())) {
            return DeviceType.ANDROID.getValue();
        }
        if (userAgent.toLowerCase().contains("iphone") || userAgent.toLowerCase().contains("ipad")) {
            return DeviceType.IOS.getValue();
        }
        if (userAgent.toLowerCase().contains("windows nt") && !userAgent.toLowerCase().contains("windows phone")) {
            return DeviceType.PC.getValue();
        }
        return DeviceType.DEFAULT.getValue();
    }
}

2、功能实现

接下来就是对登录模式的实现了,主要就是在用户登录时判断登录模式,根据登录模式的不同实现不同的登录效果即可。
下面的改动都是在 JwtUtil 类中的改动。

1.引入配置

@Value("${login.is-share}")
private Boolean isShare;

@Value("${login.login-model}")
private String loginModel;

2.功能实现

1.原方法改造

因为登录方式变了,所以我们需要重新改造之前的生成 token,验证 token 与移除 token 方法
因为生成 token 要改动的内容比较多,所以先说一下验证 token 与移除 token 方法
验证 token,这个改动不大,只是改了下 key:

public Boolean validateToken(String token) {
    // 调用这个方法主要是用来验证签名和有效期
    getClaimsByToken(token);
    String key = TokenConstant.TOKEN + token;
    return StrUtil.isNotEmpty(token) && !isTokenExpired(token) && redisUtil.hasKey(key);
}

移除 token,相比于之前,现在需要把 session 和 token 都要同时移除:

public void removeToken(String token) {
    // 获取session信息
    String account = getAccountByToken(token);
    String key = TokenConstant.TOKEN + token;
    TokenInfo tokenInfo = (TokenInfo) redisUtil.get(TokenConstant.SESSION + account);
    // 删除该token
    tokenInfo.getTokenSignList().removeIf(e -> e.getToken().equals(token));
    redisUtil.set(TokenConstant.SESSION + account, tokenInfo);
    redisUtil.del(key);
    // 如果此时该账号没有token了,则删除UserDetails
    if (CollectionUtils.isEmpty(tokenInfo.getTokenSignList())) {
        delUserDetail(account);
    }
}

最后是生成 token,这个是关键,相比与之前,就是根据不同登录方式来执行不同的方法:

public String generateToken(SysUser user) {
    String token = Jwts.builder()
            .setSubject(user.getAccount())
            .setExpiration(generateExpired())
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    String device = UserAgentUtil.getDevice();
    // 判断登录模式
    if (LoginModel.ALONE.getValue().equals(loginModel)) {
        // 单设备登录
        aloneLogin(token, user.getAccount(), device);
    } else if (LoginModel.MULTI.getValue().equals(loginModel)) {
        // 多设备登录
        String newToken = multiLogin(token, user.getAccount(), device);
        return newToken;
    } else {
        // 同端互斥登录
        sameDeviceExclusionLogin(token, user.getAccount(), device);
    }
    return token;
}

2.通用方法

在编写登录模式的方法之前,先写好一些通用的方法

  • 获取用户 session 信息:因为 session 信息包含了这个用户的所有 token,所以我们可以写一个方法用来获取,需要注意的是,因为 session 只是一个键值对,所以不能设置有效期,我们需要手动比对 token 的有效期,把过期的删掉
  • 设置token和session到redis:这里主要是把当前的 token 和更新后的 token 都重新保存到 redis 中
/**
 * 获取用户session信息
 * @param account
 * @return
 */
private TokenInfo getSessionInfo(String account) {
    String key = TokenConstant.SESSION + account;
    TokenInfo tokenInfo = (TokenInfo) redisUtil.get(key);
    if (tokenInfo == null) {
        tokenInfo = new TokenInfo();
        tokenInfo.setId(key);
        tokenInfo.setCreateTime(System.currentTimeMillis());
        List<TokenSign> tokenSignList = new ArrayList<>();
        tokenInfo.setTokenSignList(tokenSignList);
    }
    // 删除过期token
    long cur = System.currentTimeMillis();
    tokenInfo.getTokenSignList().removeIf(e -> e.getDeadline() < cur);
    return tokenInfo;
}

/**
 * 设置token和session到redis
 * @param token
 * @param device
 * @param account
 * @param sessionInfo
 */
private void setTokenAndSessionHandle(String token, String device, String account, TokenInfo sessionInfo) {
    // 将token存入
    redisUtil.set(TokenConstant.TOKEN + token, account);
    // 组装新的token
    TokenSign tokenSign = new TokenSign();
    tokenSign.setToken(token);
    tokenSign.setDevice(device);
    tokenSign.setDeadline(System.currentTimeMillis() + expiration * 1000);
    sessionInfo.getTokenSignList().add(tokenSign);
    // 把session存入
    redisUtil.set(TokenConstant.SESSION + account, sessionInfo);
}

3.新方法

接下来就是三种登录模式了。

1. 单设备登录

单设备登录需要删除原来的 token,并保存新的 token

/**
 * 单设备登录
 * @param token
 * @param account
 * @param device
 */
private void aloneLogin(String token, String account, String device) {
    // 拿到当前用户的session信息
    TokenInfo sessionInfo = getSessionInfo(account);
    List<TokenSign> tokenSignList = sessionInfo.getTokenSignList();
    // token不为空时清空所有token
    if (CollectionUtil.isNotEmpty(tokenSignList)) {
        for (TokenSign t : tokenSignList) {
            redisUtil.del(TokenConstant.TOKEN + t.getToken());
        }
        tokenSignList.clear();
    }
    setTokenAndSessionHandle(token, device, account, sessionInfo);
}

2.多设备登录

多设备登录需要注意是否是共享 token

/**
 * 多设备登录
 * @param token
 * @param account
 * @param device
 * @return token
 */
private String multiLogin(String token, String account, String device) {
    // 拿到当前用户的session信息
    TokenInfo sessionInfo = getSessionInfo(account);
    List<TokenSign> tokenSignList = sessionInfo.getTokenSignList();
    // 如果是共享token且session里面存在token
    if (isShare && CollectionUtil.isNotEmpty(tokenSignList)) {
        // 则直接返回原来的token
        return tokenSignList.get(0).getToken();
    }
    // 没有开启共享token,或开启了共享token但是是第一次登录
    setTokenAndSessionHandle(token, device, account, sessionInfo);
    return token;
}

3.同端互斥登录

需要注意同一个设备只允许一个登录

/**
 * 同端互斥登录
 * @param token
 * @param account
 * @param device
 */
private void sameDeviceExclusionLogin(String token, String account, String device) {
    TokenInfo sessionInfo = getSessionInfo(account);
    List<TokenSign> tokenSignList = sessionInfo.getTokenSignList();
    // 判断是否存在当前设备的登录信息
    Iterator<TokenSign> iterator = tokenSignList.iterator();
    while (iterator.hasNext()) {
        TokenSign next = iterator.next();
        if (next.getDevice().equals(device)) {
            // 存在则删除
            redisUtil.del(TokenConstant.TOKEN + next.getToken());
            iterator.remove();
            break;
        }
    }
    // 录入当前登录信息
    setTokenAndSessionHandle(token, device, account, sessionInfo);
}

4.踢出用户

踢出用户本质上就是把 redis 里面相应的 token 删除就可以了,这个踢出用户的方法我们可以写在 UserProvider 中。

/**
 * 踢出该账号的所有登录信息
 * @param account
 */
public void kickOutUserByAccount(String account) {
    // 拿到session信息
    TokenInfo sessionInfo = (TokenInfo) redisUtil.get(TokenConstant.SESSION + account);
    if (sessionInfo != null) {
        List<TokenSign> tokenSignList = sessionInfo.getTokenSignList();
        // 删除全部token
        for (TokenSign t : tokenSignList) {
            redisUtil.del(TokenConstant.TOKEN + t.getToken());
        }
        // 删除session里的token
        tokenSignList.clear();
        redisUtil.set(TokenConstant.SESSION + account, sessionInfo);
    }
}

/**
 * 踢出该token的登录信息
 * @param token
 */
public void kickOutUserByToken(String token) {
    // 拿到账号
    String account = (String) redisUtil.get(TokenConstant.TOKEN + token);
    // 拿到session
    TokenInfo sessionInfo = (TokenInfo) redisUtil.get(TokenConstant.SESSION + account);
    List<TokenSign> tokenSignList = sessionInfo.getTokenSignList();
    // 删除session中的token
    tokenSignList.removeIf(e -> e.getToken().equals(token));
    // 删除token
    redisUtil.del(TokenConstant.TOKEN + token);
    redisUtil.set(TokenConstant.SESSION + account, sessionInfo);
}

到这里就结束了,大家可以自己测试一下,有什么不懂了可以去 git 查看源码:点击前往

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值