【SSM开发实战:博客系统】(二)JWT 登录流程、拦截器实现和用户信息接口落地

文章目录

项目业务层

一、登录实现

1.1 令牌技术

令牌技术:

使用令牌技术,考虑下述场景时:

  1. 用户登录:用户登录请求,经过负载均衡,把请求转给了第一台服务器,第一台服务器进行账号密码验证,验证成功后,生成一个令牌,并返回给客户端。
  2. 客户端收到令牌之后,把令牌存储起来。可以存储在Cookie中,也可以存储在其他的存储空间(比如localStorage)。
  3. 查询操作:用户登录成功之后,携带令牌继续执行查询操作,比如查询博客列表。此时请求转发到了第二台机器,第二台机器会先进行权限验证操作。服务器验证令牌是否有效,如果有效,说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前未执行登录操作。


令牌优缺点:

优点:

  • 解决了集群环境下的认证问题
  • 减轻了服务器的存储压力(不用存储在服务器中)

缺点:需要自己实现(包括令牌生成,传递,校验)

1.2 JWT 令牌

介绍:JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),用于客户端和服务器之间传递安全可靠的信息。官方网址:https://www.jwt.io/

其本质是一个token,是一种紧凑的URL安全方法。

JWT组成

JWT由三部分组成,每部分中间使用点(.)分隔,比如:aaaaa.bbbbb.ccccc

  • Header(头部):头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)。
  • Payload(负载) :负载部分是存放有效信息的地方,里面是一些自定义内容,比如:{"userId":"123","userName":"zhangsan"},也可以存在JWT提供的现成字段,比如exp(过期时间戳)等。
  • Signature(签名):此部分用于防止JWT内容被篡改,确保安全性。

Signature 防止被篡改,而不是防止被解析。

JWT之所以安全,就是因为最后的签名。JWT当中任何一个字符被篡改,整个令牌都会校验失败。

就好比我们的身份证,之所以能标识一个人的身份,是因为它不能被篡改,而不是因为内容加密(任何人都可以看到身份证的信息,JWT也是)。

1.3 JWT令牌使用

用户登录按照如下功能进行:

  1. 登录页面把用户名密码提交给服务器。
  2. 服务器端验证用户名密码是否正确,如果正确,服务器生成令牌,下发给客户端。
  3. 客户端把令牌存储起来(比如Cookie、local storage等),后续请求时,把token发给服务器。
  4. 服务器对令牌进行校验,如果令牌正确,进行下一步操作。
  1. 引入JWT令牌依赖
xml 复制代码
	<dependency>
		<groupId>io.jsonwebtoken</groupId>
		<artifactId>jjwt-api</artifactId>
		<version>0.11.5</version>
	</dependency>
	<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
	<dependency>
		<groupId>io.jsonwebtoken</groupId>
		<artifactId>jjwt-impl</artifactId>
		<version>0.11.5</version>
		<scope>runtime</scope>
	</dependency>
	<dependency>
		<groupId>io.jsonwebtoken</groupId>
		<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
		<version>0.11.5</version>
		<scope>runtime</scope>
	</dependency>
  1. 约定前后端交互接口

接口信息

  • 请求路径/user/login
  • 请求方式:POST

请求参数

json 复制代码
{
  "userName": "test",
  "password": "123456"
}

响应示例

json 复制代码
{
  "code": 200,
  "errMsg": null,
  "data": {
    "userId": 1,
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6MSwiaWF0IjoxNzIxMTE5NjgyLCJleHAiOjE3MjE1MjE0ODJ9.5hwKlAh2jPPBNn3uPja4JTGguZNB3QrpRoPqCep7qME"
  }
}

说明

  • 验证成功:返回code=200及包含userIdtoken的响应数据。
  • 验证失败:返回空字符串。

创建JWT工具类:

java 复制代码
public class JwtUtils {
    private static final long EXPIRATION_TIME = 7*24*60*60*1000;
    private static final String secretString = "kIx9zHQe56aRa3D4THlPuj+ruT1yIhTAdgxRz8VLrdA=";

    private static final Key key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
    /**
     * 生成令牌
     */
    public static String genJwt(Map<String,Object> map){
        return Jwts.builder()
                .setClaims(map)
                .signWith(key, SignatureAlgorithm.HS256)
                .setExpiration(new Date(System.currentTimeMillis()+EXPIRATION_TIME))
                .compact();
    }

    /**
     * 校验令牌
     */
    public static Boolean check(String token){
        if(!StringUtils.hasLength(token)){
            return false;
        }
        JwtParser build =  Jwts.parserBuilder().setSigningKey(key).build();
        try {
            Claims body = build.parseClaimsJws(token).getBody();
            return true;
        }catch (Exception e){
            return false;
        }
    }
}
  1. 实现Controller 层
java 复制代码
	@PostMapping("/login")
    public UserLoginResponse login(@Validated @RequestBody UserLoginRequest loginRequest){
        log.info("用户登录, username:{}",loginRequest.getUserName());
        // 参数校验
        return userService.check(loginRequest);
    }
  1. 实现Service 层
java 复制代码
	@Override
    public UserLoginResponse check(UserLoginRequest loginRequest) {
        // 判断密码是否正确
        UserInfo userInfo = selectUserInfo(loginRequest.getUserName());
        if(userInfo==null) {
            throw new BlogException("用户不存在");
        }
        if(!loginRequest.getPassword().equals(userInfo.getPassword())){
            throw new BlogException("密码不正确");
        }
        Map<String,Object> map = new HashMap<>();
        map.put("id",userInfo.getId());
        map.put("name",userInfo.getUserName());
        String token = JwtUtils.genJwt(map);
        return new UserLoginResponse(userInfo.getId(),token);
    }

测试后端接口返回正确,token已返回:

二、强制登录(拦截器)

当用户访问博客列表和详情页等时,如果用户尚未登录,就自动跳转到登录页面。使用拦截器来完成,从前端header中获取token,然后进行校验token是否合法。如果合法则正常访问,如果不合法则强制回到登录页。

添加拦截器:

java 复制代码
	@Slf4j
	public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取token
        String userToken = request.getHeader(Constants.HEADER_USER_TOKEN_KEY);

        log.info("从header中获取到token,url:{},token:{}",request.getRequestURI(),userToken);
         // 校验token
        Boolean check = JwtUtils.check(userToken);
        if(check){
            return true;
        }
        log.error("token检验失败,token:{}",userToken);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        return false;
    }
}

代码中HEADER_USER_TOKEN_KEY静态变量需要和前端进行对应相等。

此外,添加Config类实现WebMvcConfigurer 来排掉不需要拦截的路径。

验证发现,当token为空或者不合法时,强制跳转到登录页。

三、显示用户信息

用户信息希望可以随着用户登录而改变,并且如果当前页面为博客列表页,则显示当前登录用户的信息。如果当前页面是博客详情页,则显示该博客的作者用户信息。

  1. 约定前后端交互接口

获取登录用户信息:

  • 请求路径/user/getUserInfo
  • 请求方式:GET
  • 请求参数userId=1

响应示例

json 复制代码
{
  "id": 1,
  "username": "test",
  "githubUrl": "bite.gitee.com"
}

获取博客作者信息:

  • 请求路径/user/getAuthorInfo
  • 请求方式:GET
  • 请求参数blogId=1
  • 说明:在博客详情页,获取当前文章作者的用户信息

响应示例

json 复制代码
{
  "id": 1,
  "username": "test",
  "githubUrl": "bite.gitee.com"
}
  1. 实现Controller 层:
java 复制代码
	@GetMapping("/getUserInfo")
    public UserInfoResponse getUserInfo(@NotNull Integer userId){
        log.info("获取用户信息,userId:{}",userId);
        return userService.getUserInfo(userId);
    }

    @GetMapping("/getAuthorInfo")
    public UserInfoResponse getAuthorInfo(@NotNull Integer blogId){
        log.info("获取博客的作者信息,blogId:{}",blogId);
        return userService.getAuthorInfo(blogId);
    }
  1. 实现Service 层
java 复制代码
	@Override
    public UserInfoResponse getUserInfo(Integer userId) {
        return BeanTransUtils.transUserInfo(selectUserInfoById(userId));
    }

    @Override
    public UserInfoResponse getAuthorInfo(Integer blogId) {
        BlogInfo blogInfo = blogService.getBlogInfo(blogId);
        if(blogInfo == null){
            throw new BlogException("博客不存在");
        }
        return BeanTransUtils.transUserInfo(selectUserInfoById(blogInfo.getUserId()));
    }

service 层还需进行实体数据的转换,因为接口需要的实体类和数据库定义的实体类不一致。数据转换已单独封装到公共模块里,代码与上一篇博客中的示例类似。

验证接口发现正确返回对应的用户信息:

四、用户登出

用户登出操作只需将客户端保存的token删除掉即可, 并且返回到登录界面。

前端代码中添加了onclick事件,当点击时去除token即可。

相关推荐
1104.北光c°2 小时前
【黑马点评项目笔记 | 优惠券秒杀篇】构建高并发秒杀系统
java·开发语言·数据库·redis·笔记·spring·nosql
是阿楷啊2 小时前
Java求职面试实录:互联网大厂场景技术点解析
java·redis·websocket·spring·互联网·大厂面试·支付系统
人道领域2 小时前
SSM从入门到入土(Spring Bean实例化与依赖注入全解析)
java·开发语言·spring boot·后端
long3162 小时前
Z算法(线性时间模式搜索算法)
java·数据结构·spring boot·后端·算法·排序算法
没有bug.的程序员2 小时前
Istio 服务网格:流量治理内核、故障注入实战与云原生韧性架构深度指南
spring boot·云原生·架构·istio·流量治理·故障注入·韧性架构
fengxin_rou2 小时前
[Redis从零到精通|第三篇]:缓存更新指南
java·数据库·redis·spring·缓存
李慕婉学姐3 小时前
Springboot眼镜店管理系统ferchy1l(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
Pluto_CSND3 小时前
MyBatis 的 XML 文件中特殊字符的处理
mybatis
常利兵3 小时前
Spring Boot 3 多数据源整合 Druid:监控页面与控制台 SQL 日志配置实战
android·spring boot·sql