SpringBoot+JWT实现注册、登录、状态续签【登录保持】


一、实现流程

1.注册

Created with Raphaël 2.3.0 开始 前端:传入 User(username, password) 后端:校验 username, password 是否合法 后端:查询数据库, 判断当前用户名是否唯一 后端:password 非对称加密 RSA(password) 后端:存入数据库 后端:注册成功,返回成功信息 结束 后端:注册失败,返回失败信息 yes no yes no

2.登录

Created with Raphaël 2.3.0 开始 前端:传入 User(username, password) 后端:校验 username, password 是否合法 后端:password 非对称加密 RSA(password) 后端:查询数据库中是否有此用户 后端:根据用户信息 和 秘钥 生成 JWT 后端:登录成功,返回 JWT 前端:JWT 存入 localStorage 结束 后端:登录失败,返回失败信息 yes no yes no

3.登录保持【状态续签】

Created with Raphaël 2.3.0 开始 前端: 请求业务 api【请求头携带 JWT】 后端:拦截器校验 JWT 【用户合法未超时】 后端:拦截器判断 JWT 存在时间是否超过 过期时间的一半 后端:更新 token 加入返回头 后端:执行业务 后端:返回成功信息 前端:将返回头的 JWT 覆盖 localStorage 中的 JWT 结束 后端:执行业务 后端:返回成功信息 后端:返回失败信息 yes no yes no

二、实现方法

项目结构
在这里插入图片描述

1.引入依赖

<!-- spring-web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql连接器 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Mybatis Plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3.4</version>
</dependency>
<!-- druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>
<!-- jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

2.application配置文件


spring:
  application:
    name: jwtLogin   # 应用名称
  datasource:
    druid:
      url: jdbc:mysql://192.168.0.111:3306/login_test?useSSL=false&serverTimezone=UTC
      username: samon
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver


server:
  port: 8848   # 应用服务 WEB 访问端口

3.mysql建表

用户表
在这里插入图片描述

4.Bean

  1. bean/user.java
    用户bean
package com.cxstar.bean;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

import java.util.Date;

@Data
public class User {
    // 自增长id
    @TableId(type = IdType.AUTO)
    private Integer id;

    private String userName;
    private String passWord;
    private Date createTime;
    private Date lastLogin;

    public  User() {}

    public User(Integer id, String userName) {
        this.id = id;
        this.userName = userName;
    }
}

  1. bean/ServiceRes.java
    统一Service返回类
package com.cxstar.bean;

import lombok.Data;

@Data
public class ServiceRes {
    private Integer code;
    private String msg;
    private String jwt;

    private ServiceRes() {}

    public ServiceRes(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public ServiceRes(Integer code, String msg, String jwt) {
        this.code = code;
        this.msg = msg;
        this.jwt = jwt;
    }
}

  1. bean/ControllerRes.java
    统一Controller返回类
package com.cxstar.bean;

import lombok.Data;

@Data
public class ControllerRes {
    private Integer code;
    private String msg;

    public ControllerRes(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

5.Mapper

  1. mapper/UserMapper.java
    继承MP
package com.cxstar.mapper;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cxstar.bean.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {

}

6.service

  1. service.userService.interface
    登录、注册、改密业务【状态续签测试】
package com.cxstar.service;

import com.cxstar.bean.ServiceRes;
import com.cxstar.bean.User;

public interface userService {

    // 注册
    ServiceRes register(User user);

    // 登录
    ServiceRes login(User user);

    // 改密【带权限业务,用于状态续签测试】
    ServiceRes changePassWord(User user);

}

  1. service.impl.UserServiceImpl.java
    登录、注册、改密业务【状态续签测试】
package com.cxstar.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cxstar.bean.ServiceRes;
import com.cxstar.bean.User;
import com.cxstar.mapper.UserMapper;
import com.cxstar.service.userService;
import com.cxstar.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class UserServiceImpl implements userService {

    @Autowired
    UserMapper userMapper;

    /**
     * 注册
     * @param user 用户类
     * @return ServiceRes
     */
    @Override
    public ServiceRes register(User user) {

        // 判断用户名是否唯一
        if(this.checkUserNameIsUnique(user)) {

            // 判断用户名密码是否合法
            if(this.checkUserNameAndPassword(user)) {

                // 密码MD5加密
                user.setPassWord(this.MD5Code(user.getPassWord()));
                // 加入创建时间
                user.setCreateTime(new Date());
                // 入库
                userMapper.insert(user);
                return new ServiceRes(1, "注册成功");

            } else return new ServiceRes(-1, "用户名或密码不合法");

        } else return new ServiceRes(-1, "用户名已存在");

    }

    /**
     * 登录
     * @param user 用户类
     * @return ServiceRes
     */
    @Override
    public ServiceRes login(User user) {

        // 判断用户名密码是否合法
        if(this.checkUserNameAndPassword(user)) {

            // 密码MD5加密
            user.setPassWord(this.MD5Code(user.getPassWord()));

            // 检查用户是否存在
            User curUser = this.checkUserIsExit(user);
            if(curUser!=null) {

                // 更新用户最后登录时间
                curUser.setLastLogin(new Date());
                userMapper.updateById(curUser);

                // 生成jwt
                Map<String, String> payload = new HashMap<>();
                payload.put("userId", curUser.getId().toString()); // 加入一些非敏感的用户信息
                payload.put("userName", curUser.getUserName());    // 加入一些非敏感的用户信息
                String jwt = JwtUtil.generateToken(payload);
                return new ServiceRes(1, "登录成功", jwt);

            } else return new ServiceRes(-1, "用户名或密码错误");

        } else return new ServiceRes(-1, "用户名或密码不合法");

    }

    /**
     * 改密业务
     * @return ServiceRes
     */
    @Override
    public ServiceRes changePassWord(User user) {
        if(this.updatePassWord(user)) return new ServiceRes(1, "改密成功");
        else return new ServiceRes(-1, "改密失败");
    }

    /**
     * 非对称加密
     * @param text 明文
     * @return 密文
     */
    private String MD5Code(String text) {
        return DigestUtils.md5DigestAsHex(text.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * 修改密码方法
     * @param user 传入用户名和新密码
     * @return 改密成功返回 true 失败返回 false
     */
    private Boolean updatePassWord(User user) {
        // 密码非对称加密
        user.setPassWord(this.MD5Code(user.getPassWord()));
        // 更新密码
        return userMapper.updateById(user)>0;
    }

    /**
     * 检查用户是否存在【用户名密码相同】
     * @param user 用户类
     * @return 用户存在返回 用户对象 不存在返回 null
     */
    private User checkUserIsExit(User user) {
        LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
        lqw.eq(User::getUserName, user.getUserName());
        lqw.eq(User::getPassWord, user.getPassWord());
        return userMapper.selectOne(lqw);
    }

    /**
     * 判断用户名是否唯一
     * @param user 用户类
     * @return 唯一返回 true 不唯一返回 false
     */
    private Boolean checkUserNameIsUnique(User user) {
        LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
        lqw.eq(User::getUserName, user.getUserName());
        List<User> userList = userMapper.selectList(lqw);
        return userList.size() == 0;
    }

    /**
     * 判断用户名密码是否合法
     * @param user 用户类
     * @return 满足 【英文字母、数字、下划线】 返回 true,否则返回 false
     */
    private Boolean checkUserNameAndPassword(User user) {
        String regex = "^[_a-z0-9A-Z]+$";
        return user.getUserName().matches(regex) && user.getPassWord().matches(regex);
    }


}


6.Controller

  1. controller/UserController.java
    登录、注册、改密业务【状态续签测试】
package com.cxstar.controller;

import com.cxstar.bean.ControllerRes;
import com.cxstar.bean.ServiceRes;
import com.cxstar.bean.User;
import com.cxstar.service.userService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {

    @Autowired
    userService userService;

    @PostMapping("/register")
    public ControllerRes register(User user) {

        // 注册
        ServiceRes serviceRes = userService.register(user);

        return new ControllerRes(serviceRes.getCode(), serviceRes.getMsg());
    }

    @PostMapping("/login")
    public ControllerRes login(User user, HttpServletResponse response) {

        // 登录
        ServiceRes serviceRes = userService.login(user);

        // 登录成功后往响应头插入jwt
        if(serviceRes.getJwt() != null) response.addHeader("access-token", serviceRes.getJwt());

        return new ControllerRes(serviceRes.getCode(), serviceRes.getMsg());
    }

    @PutMapping("/pwd")
    public ControllerRes changePassWord(User user, HttpServletRequest request) {

        // 取出jwt中的用户
        User jwtUser = (User)request.getAttribute("jwt-user");

        // 合并jwt中用户的用户名与传入用户的新密码
        // 此处不能直接使用传入的用户名,防止恶意修改其他用户的密码
        user.setId(jwtUser.getId());

        // 改密
        ServiceRes serviceRes = userService.changePassWord(user);

        return new ControllerRes(serviceRes.getCode(), serviceRes.getMsg());

    }

}

7.JWT工具类

  1. utils/JwtUtil.java
    生成和解析 token 的方法
package com.cxstar.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;

import java.util.Calendar;
import java.util.Date;
import java.util.Map;

public class JwtUtil {
    // 签名密钥
    private static final String SECRET = "hello JWT *%$#$&";

    /**
     * 生成token
     * @param payload token携带的信息
     * @return token字符串
     */
    public static String generateToken(Map<String,String> payload){
        // 指定token过期时间
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.HOUR, 24);  // 24小时

        JWTCreator.Builder builder = JWT.create();
        // 构建payload
        payload.forEach(builder::withClaim);
        // 指定签发时间、过期时间 和 签名算法,并返回token
        String token = builder.withIssuedAt(new Date()).withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(SECRET));
        return token;
    }


    /**
     * 解析token
     * @param token token字符串
     * @return 解析后的token类
     */
    public static DecodedJWT decodeToken(String token){
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
        DecodedJWT decodedJWT = jwtVerifier.verify(token);
        return decodedJWT;
    }
}


8.HandlerInterceptor拦截器

  1. interceptor.java
    拦截器业务实现
package com.cxstar.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.cxstar.bean.ControllerRes;
import com.cxstar.bean.User;
import com.cxstar.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 拦截需要授权的接口
 */
@Slf4j
public class PermisssionInterceptor implements HandlerInterceptor {

    // 目标方法执行前调用
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        // 检查用户JWT
        String jwt = request.getHeader("access-token");

        // 校验并取出私有信息
        try {

            // token 解码
            DecodedJWT dj = JwtUtil.decodeToken(jwt);

            // 取出基本用户信息加入请求头 --------------------------------------------------------------------------------
            String userId = dj.getClaim("userId").asString();
            String userName = dj.getClaim("userName").asString();
            // jwt校验合格的,将 jwt 中存的用户信息加入请求头,不合格的,请求头存个空用户
            request.setAttribute("jwt-user", userId!=null?new User(Integer.valueOf(userId), userName):new User());
            // -------------------------------------------------------------------------------------------------------

            // 计算当前时间是否超过过期时间的一半,如果是就帮用户续签 --------------------------
            // 此处并不是永久续签,只是为 大于过期时间的一半 且 小于过期时间 的 token 续签
            Long expTime = dj.getExpiresAt().getTime();
            Long iatTime = dj.getIssuedAt().getTime();
            Long nowTime = new Date().getTime();
            if((nowTime-iatTime) > (expTime-iatTime)/2) {
                // 生成新的jwt
                Map<String, String> payload = new HashMap<>();
                payload.put("userId", userId); // 加入一些非敏感的用户信息
                payload.put("userName", userName);    // 加入一些非敏感的用户信息
                String newJwt = JwtUtil.generateToken(payload);
                // 加入返回头
                response.addHeader("access-token", newJwt);
            }
            // -----------------------------------------------------------------------

            return true;

        } catch (JWTDecodeException e) {
            log.error("令牌错误");
            addResBody(response, new ControllerRes(-1, "令牌错误"));  // 新增返回体
            return false;

        } catch (TokenExpiredException e) {
            log.error("令牌过期");
            addResBody(response, new ControllerRes(-1, "令牌过期"));  // 新增返回体
            return false;
        }

    }

    // 目标方法执行后调用
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    // 页面渲染前调用
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

    private void addResBody(HttpServletResponse response, ControllerRes res) throws IOException {

        response.setStatus(HttpServletResponse.SC_FORBIDDEN);        // 设置状态码

        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write(JSONObject.toJSONString(res));
        out.flush();
        out.close();

    }

}

  1. config/PermissionWebConfig.java
    拦截器拦截规则
package com.cxstar.config;

import com.cxstar.interceptor.PermisssionInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class PermissionWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new PermisssionInterceptor())
                .addPathPatterns("/**")    // 拦截哪些页面
                .excludePathPatterns("/user/login", "/user/register");   // 放行哪些页面
    }
}


三、测试

1.注册

  1. 注册成功
    在这里插入图片描述
  2. 数据入库
    在这里插入图片描述

2.登录

  1. 登录成功
    在这里插入图片描述
  2. 查看登录后返回的token
    在这里插入图片描述

3.状态续签【登录保持】

  1. 使用上一步登录返回的 token 请求改密业务
    在这里插入图片描述

  2. 当 JWT 存在时间小于 JWT 过期时间的一半时
    业务会执行成功
    执行业务不会返回续签的 token
    在这里插入图片描述

  3. 当 JWT 存在时间大于 JWT 过期时间的一半 且 小于过期时间 时
    业务会执行成功
    执行业务会返回续签的 token,前端的下次请求需要使用新续签的 token
    在这里插入图片描述

  4. 当 JWT 存在时间大于 JWT 过期时间 时
    业务会执行失败
    执行业务不会返回续签的 token
    在这里插入图片描述

  • 14
    点赞
  • 62
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
单点登录(Single Sign-On,简称SSO)是一种身份验证技术,可以让用户只需一次登录,就可以访问多个应用程序。在实际开发中,我们可以使用Spring Boot、JWT和Redis来实现单点登录功能。 下面是实现单点登录的步骤: 1. 创建Spring Boot项目并引入所需依赖:spring-boot-starter-web、spring-boot-starter-data-redis和jjwt。 2. 创建一个User实体类,包含用户名和密码等信息。 3. 创建一个UserService,实现对用户信息的操作,包括注册登录等。 4. 引入JWT依赖后,我们需要创建一个JWTUtil类,实现token的生成和解析。 5. 创建一个LoginController,用于处理用户的登录请求。在登录成功后,生成token并将其存储到Redis中。 6. 创建一个AuthController,用于验证用户的token是否有效。在验证成功后,可以获取用户信息并返回。 7. 在需要进行单点登录验证的应用程序中,只需要在请求中携带token,并调用AuthController进行验证即可。 具体实现细节可以参考以下代码示例: User实体类: ```java public class User { private String username; private String password; // 省略setter和getter方法 } ``` UserService接口: ```java public interface UserService { void register(User user); String login(String username, String password); } ``` UserService实现类: ```java @Service public class UserServiceImpl implements UserService { @Autowired private RedisTemplate<String, String> redisTemplate; @Override public void register(User user) { // 省略用户注册逻辑 } @Override public String login(String username, String password) { // 省略用户登录逻辑 // 登录成功后生成token并存储到Redis中 String token = JWTUtil.generateToken(username); redisTemplate.opsForValue().set(username, token, 30, TimeUnit.MINUTES); return token; } } ``` JWTUtil类: ```java public class JWTUtil { private static final String SECRET_KEY = "my_secret_key"; private static final long EXPIRATION_TIME = 3600000; public static String generateToken(String username) { return Jwts.builder() .setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static String getUsernameFromToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody() .getSubject(); } } ``` LoginController: ```java @RestController public class LoginController { @Autowired private UserService userService; @PostMapping("/login") public ResponseEntity<String> login(@RequestBody User user) { String token = userService.login(user.getUsername(), user.getPassword()); return ResponseEntity.ok(token); } } ``` AuthController: ```java @RestController public class AuthController { @Autowired private RedisTemplate<String, String> redisTemplate; @GetMapping("/auth") public ResponseEntity<User> auth(@RequestHeader("Authorization") String token) { String username = JWTUtil.getUsernameFromToken(token); if (StringUtils.isEmpty(username)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } String redisToken = redisTemplate.opsForValue().get(username); if (!token.equals(redisToken)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } User user = new User(); user.setUsername(username); return ResponseEntity.ok(user); } } ``` 在请求中携带token的示例: ```java @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.setInterceptors(Collections.singletonList((request, body, execution) -> { String token = // 从Redis中获取token request.getHeaders().add("Authorization", token); return execution.execute(request, body); })); return restTemplate; } } ``` 以上就是使用Spring Boot、JWT和Redis实现单点登录的步骤和示例代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

什么都干的派森

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值