JWT登录解决方案
session共享、分布式缓存
- 单机tomcat应用登录检验
- sesssion保存在浏览器和应用服务器会话之间
- 用户登录成功,服务端会保存一个session,当然客户端有一个sessionId
- 客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId
- 分布式应用中session共享
- 真实的应用不可能单节点部署,所以就有个多节点登录session共享的问题需要解决
- tomcat支持session共享,但是有广播风暴;用户量大的时候,占用资源就严重,不推荐
- 使用redis存储token:
- 服务端使用UUID生成随机64位或者128位token,放入redis中,然后返回给客户端并存储在cookie中
- 用户每次访问都携带此token,服务端去redis中校验是否有此用户即可
分布式应用下登录检验解决方案 JWT
-
什么是JWT
- JWT 是一个开放标准,它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名
- 简单来说: 就是通过一定规范来生成token,然后可以通过解密算法逆向解密token,这样就可以获取用户信息
-
优点
- 生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库
- 存储在客户端,不占用服务端的内存资源
-
缺点
- token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,如用户权限,密码等
- 如果没有服务端存储,则不能做登录失效处理,除非服务端改秘钥
{
id:888,
name:'小D',
expire:10000
}
funtion 加密(object, appsecret){
xxxx
return base64( token);
}
function 解密(token ,appsecret){
xxxx
//成功返回true,失败返回false
}
-
JWT格式组成 头部、负载、签名
- header+payload+signature
- 头部:主要是描述签名算法
- 负载:主要描述是加密对象的信息,如用户的id等,也可以加些规范里面的东西,如iss签发者,exp 过期时间,sub 面向的用户
- 签名:主要是把前面两部分进行加密,防止别人拿到token进行base解密后篡改token
- header+payload+signature
-
关于jwt客户端存储
- 可以存储在cookie,localstorage和sessionStorage里面
JsonWebToken工具类封装
package net.xdclass.online_xdclass.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import net.xdclass.online_xdclass.domain.User;
import java.util.Date;
/**
* Jwt工具类
* 注意点:
* 1、生成的token, 是可以通过base64进行解密出明文信息
* 2、base64进行解密出明文信息,修改再进行编码,则会解密失败
* 3、无法作废已颁布的token,除非改秘钥
*/
public class JWTUtils {
/**
* 过期时间,一周
*/
private static final long EXPIRE = 60000 * 60 * 24 * 7;
/**
* 加密秘钥
*/
private static final String SECRET = "xdclass.net168";
/**
* 令牌前缀
*/
private static final String TOKEN_PREFIX = "xdclass";
/**
* subject
*/
private static final String SUBJECT = "xdclass";
/**
* 根据用户信息,生成令牌
* @param user
* @return
*/
public static String geneJsonWebToken(User user){
String token = Jwts.builder().setSubject(SUBJECT)
.claim("head_img",user.getHeadImg())
.claim("id",user.getId())
.claim("name",user.getName())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.signWith(SignatureAlgorithm.HS256,SECRET).compact();
token = TOKEN_PREFIX + token;
return token;
}
/**
* 校验token的方法
* @param token
* @return
*/
public static Claims checkJWT(String token){
try{
final Claims claims = Jwts.parser().setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKEN_PREFIX,"")).getBody();
return claims;
}catch (Exception e){
//解密失败返回Null
return null;
}
}
}
测试
src/test/java/net/xdclass/online_xdclass/OnlineXdclassApplicationTests.java
package net.xdclass.online_xdclass;
import io.jsonwebtoken.Claims;
import net.xdclass.online_xdclass.model.entity.User;
import net.xdclass.online_xdclass.utils.JWTUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;
@SpringBootTest
class OnlineXdclassApplicationTests {
@Test
public void testGeneJwt(){
User user = new User();
user.setId(66);
user.setName("二当家小D");
user.setHeadImg("png");
String token = JWTUtils.geneJsonWebToken(user);
System.out.println(token);
// try {
// Thread.sleep(4000L);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
Claims claims = JWTUtils.checkJWT(token);
System.out.println(claims.get("name"));
}
}
登录模块整合Json Web Token
Model
在model下增加request包,并创建登录对象 LoginRequest
package net.xdclass.online_xdclass.model.request;
/**
* 登录 request
*/
public class LoginRequest {
private String phone;
private String pwd;
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
}
Controller
package net.xdclass.online_xdclass.controller;
import net.xdclass.online_xdclass.model.request.LoginRequest;
import net.xdclass.online_xdclass.service.UserService;
import net.xdclass.online_xdclass.utils.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("api/v1/pri/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 注册接口
* @param userInfo
* @return
*/
@PostMapping("register")
public JsonData register(@RequestBody Map<String,String> userInfo ){
int rows = userService.save(userInfo);
return rows == 1 ? JsonData.buildSuccess(): JsonData.buildError("注册失败,请重试");
}
/**
* 登录接口
* @param loginRequest
* @return
*/
@PostMapping("login")
public JsonData login(@RequestBody LoginRequest loginRequest){
String token = userService.findByPhoneAndPwd(loginRequest.getPhone(), loginRequest.getPwd());
return token == null ?JsonData.buildError("登录失败,账号密码错误"): JsonData.buildSuccess(token);
}
}
Service
interface
package net.xdclass.online_xdclass.service;
import java.util.Map;
public interface UserService {
/**
* 新增用户
* @param userInfo
* @return
*/
int save(Map<String, String> userInfo);
String findByPhoneAndPwd(String phone, String pwd);
}
impl
package net.xdclass.online_xdclass.service.impl;
import net.xdclass.online_xdclass.model.entity.User;
import net.xdclass.online_xdclass.mapper.UserMapper;
import net.xdclass.online_xdclass.service.UserService;
import net.xdclass.online_xdclass.utils.CommonUtils;
import net.xdclass.online_xdclass.utils.JWTUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Map;
import java.util.Random;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public int save(Map<String, String> userInfo) {
User user = parseToUser(userInfo);
if( user != null){
return userMapper.save(user);
}else {
return -1;
}
}
@Override
public String findByPhoneAndPwd(String phone, String pwd) {
User user = userMapper.findByPhoneAndPwd(phone, CommonUtils.MD5(pwd));
if(user == null){
return null;
}else {
String token = JWTUtils.geneJsonWebToken(user);
return token;
}
}
/**
* 解析 user 对象
* @param userInfo
* @return
*/
private User parseToUser(Map<String,String> userInfo) {
if(userInfo.containsKey("phone") && userInfo.containsKey("pwd") && userInfo.containsKey("name")){
User user = new User();
user.setName(userInfo.get("name"));
user.setHeadImg(getRandomImg());
user.setCreateTime(new Date());
user.setPhone(userInfo.get("phone"));
String pwd = userInfo.get("pwd");
//MD5加密
user.setPwd(CommonUtils.MD5(pwd));
return user;
}else {
return null;
}
}
/**
* 放在CDN上的随机头像
*/
private static final String [] headImg = {
"https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/12.jpeg",
"https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/11.jpeg",
"https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/13.jpeg",
"https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/14.jpeg",
"https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/15.jpeg"
};
private String getRandomImg(){
int size = headImg.length;
Random random = new Random();
int index = random.nextInt(size);
return headImg[index];
}
}
Mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="net.xdclass.online_xdclass.mapper.UserMapper">
<insert id="save" parameterType="User">
INSERT INTO user (name, pwd, head_img, phone , create_time)
values (#{name,jdbcType=VARCHAR}, #{pwd,jdbcType=VARCHAR}, #{headImg,jdbcType=VARCHAR},
#{phone,jdbcType=VARCHAR},#{createTime,jdbcType=TIMESTAMP})
</insert>
<!--根据手机号查询用户信息-->
<select id="findByPhone" resultType="User">
select * from user where phone =#{phone}
</select>
<!--根据手机号和密码找用户-->
<select id="findByPhoneAndPwd" resultType="User">
select * from user where phone =#{phone} and pwd = #{pwd}
</select>
</mapper>
测试
localhost:8081/api/v1/pri/user/login
POST:Body->raw->JSON
{
"phone":"12345678",
"pwd":"111111"
}
***Response***
{
"code": 0,
"data": "xdclasseyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4ZGNsYXNzIiwiaGVhZF9pbWciOiJodHRwczovL3hkLXZpZGVvLXBjLWltZy5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20veGRjbGFzc19wcm8vZGVmYXVsdC9oZWFkX2ltZy8xNS5qcGVnIiwiaWQiOjgsIm5hbWUiOiJhbGljZSIsImlhdCI6MTYxMDUwMzM5NSwiZXhwIjoxNjExMTA4MTk1fQ.Hru8k9bwXd32t2iQX-6br_30M-HwiYKVerfChv_Recw",
"msg": null
}