环境介绍
技术栈 | springboot+mybatis-plus+mysql+java-jwt |
软件 | 版本 |
mysql | 8 |
IDEA | IntelliJ IDEA 2022.2.1 |
JDK | 1.8 |
Spring Boot | 2.7.13 |
mybatis-plus | 3.5.3.2 |
Json Web令牌简称JWT
Token是在服务端产生的一串字符串是客户端访问资源接口(AP)时所需要的资源凭证。
Token认证
Token是在服务端产生的一串字符串是客户端访问资源接口(AP)时所需要的资源凭证。
Token认证流程
1、客户端使用用户名跟密码请求登录,服务端收到请求,验证用户名与密码验证成功后,服务端会签发一个 token并把这个 token发送给客户端,客户端收到 token后,会把它存储起来,比如放在cookie里或者localStorage里
2、客户端每次向服务端请求资源的时候需要带着服务端签发的 token
3、服务端收到请求,然后去验证客户端请求里面带着的 token,如果验证成功就向客户端返回请求的数据
token用户认证是一种服务端无状态的认证方式,服务端不用存放token数据。
用解析 token的计算时间换取 session的存储空间,从而减服务器的力,减少频繁的查询数据库
token完全由应用管理,所以它可以避开同源策略
JWT的使用
JSON Web Token(简称JWT)是一个 token的具体实现方式,是目前最流行的跨域认证解决方案。JWT的原理是:服务器认证以后,生成一个JSON对象,发回给用户。
{
“name” :”张三”,
“time”:”2022年10月10日”
}
用户与服务端通信时,都要发回该JSON对象。服务器完全只靠这个对象认定用户身份。
为防止用户篡改数据,服务器在生成对象时,会加上签名
JWT由三个部分组成:Header(头部)、Payload(负载)、Signature(签名)
Header.Payload.Signature
官方描述
Header
JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存
{
"alg": "HS256",
"typ": "JWT"
}
Payload
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
Signature
签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。
加入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.3.0</version>
</dependency>
数据库
实体类
package com.example.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
/**
*
* @TableName user
*/
@TableName(value ="user")
@Data
public class User implements Serializable {
/**
* 用户id
*/
@TableId(type = IdType.AUTO)
private Integer uid;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 盐值
*/
private String salt;
/**
* 电话号码
*/
private String phone;
/**
* 电子邮箱
*/
private String email;
/**
* 性别:0-女,1-男
*/
private Integer gender;
/**
* 头像
*/
private String avatar;
/**
* 是否删除:0-未删除,1-已删除
*/
private Integer isDelete;
/**
* 日志-创建人
*/
private String createdUser;
/**
* 日志-创建时间
*/
private Date createdTime;
/**
* 日志-最后修改执行人
*/
private String modifiedUser;
/**
* 日志-最后修改时间
*/
private Date modifiedTime;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
mapper(dao)
package com.example.mapper;
import com.example.domain.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 插入用户数据
* @param user 用户数据
* @return 受影响的行数
*/
int insert(User user);
/**
* 根据用户名查询用户是否存在
* @param username
* @return 成功返回单个用户数据,否返回null
*/
User findByUserName(@Param("username") String username);
/**
* 根据uid查询
* @param uid
* @return
*/
User findByUid(@Param("uid") Integer uid);
/**
* 更新用户个人资料信息
* @param user
* @return
*/
Integer updateUserInfoByUid(User user);
/**
* 根据用户id修改密码
* @param uid
* @return password=?,modified_user=?,modified_time=?
*/
Integer updatePasswordByUid(@Param("uid")Integer uid,
@Param("password")String password,
@Param("modified_user")String modified_user,
@Param("modified_time") Date modified_time);
}
UserMapper.xml
<?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="com.example.mapper.UserMapper">
<resultMap id="UserPojoMap" type="com.example.domain.User">
<id column="uid" property="uid"></id>
<result column="is_delete" property="isDelete"></result>
<result column="created_user" property="createdUser"></result>
<result column="created_time" property="createdTime"></result>
<result column="modified_user" property="modifiedUser"></result>
<result column="modified_time" property="modifiedTime"></result>
</resultMap>
<!-- useGeneratedKeys="true" 开启主键自增 keyProperty="uid" 指定uid字段-->
<insert id="insert" useGeneratedKeys="true" keyProperty="uid">
insert into user(username,password,salt,phone,email,gender,avatar,is_delete,
created_user,created_time,modified_user,modified_time)
values(#{username},#{password},#{salt},#{phone},#{email},#{gender},#{avatar},#{isDelete},
#{createdUser},#{createdTime},#{modifiedUser},#{modifiedTime})
</insert>
<select id="findByUserName" resultMap="UserPojoMap">
select * from user where username=#{username}
</select>
<select id="findByUid" resultMap="UserPojoMap">
select * from user where uid=#{uid}
</select>
<update id="updatePasswordByUid">
update user set password=#{password},
modified_user=#{modified_user},
modified_time=#{modified_time} where uid=#{uid}
</update>
<update id="updateUserInfoByUid">
update user set
<if test="phone!=null">phone=#{phone},</if>
<if test="email!=null">email=#{email},</if>
<if test="gender!=null">gender=#{gender},</if>
modified_user=#{modifiedUser}, modified_time=#{modifiedTime} where uid=#{uid}
</update>
<sql id="Base_Column_List">
uid,username,password,
salt,phone,email,
gender,avatar,is_delete,
created_user,created_time,modified_user,
modified_time
</sql>
</mapper>
service
package com.example.service;
import com.example.domain.User;
import com.baomidou.mybatisplus.extension.service.IService;
public interface UserService extends IService<User> {
/**
* 用户注册方法
* @param user
*/
void reg(User user);
/**
* 用户登入方法
* @param username
* @param password
* @return
*/
User login(String username,String password);
/**
* 根据uid查询
* @param uid
* @return User
*/
/**
*
* @param uid
* @param username
* @param oldPassword
* @param newPassword
*/
void changePassword(Integer uid,
String username,
String oldPassword,
String newPassword);
/**
* 通过uid获取用户数据
* @param uid
* @return
*/
User getUserInfoByUid(Integer uid);
/**
* 修改用户信息
* @param user
* @return
*/
void changeUserInfo(Integer uid,String username,User user);
}
ServiceImpl
package com.example.service.impl;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.domain.User;
import com.example.service.UserService;
import com.example.mapper.UserMapper;
import com.example.service.exception.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.util.Date;
import java.util.UUID;
@Service
@DS("wms")
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService{
@Autowired
private UserMapper userDao;
@Override
public void reg(User user) {
//判断用户是否被注册过
String username = user.getUsername();
User byUserName = userDao.findByUserName(username);
if (byUserName == null){
//密码的加密处理:MD5算法
//盐值+password+盐值
String oldPassword =user.getPassword();
//获取盐值
String salt = UUID.randomUUID().toString().toUpperCase();
//保存盐值
user.setSalt(salt);
String newPassword = getMD5Password(oldPassword,salt);
user.setPassword(newPassword);
//用户注册
// is_delete INT COMMENT '是否删除:0-未删除,1-已删除',
// created_user VARCHAR(20) COMMENT '日志-创建人',
// created_time DATETIME COMMENT '日志-创建时间',
// modified_user VARCHAR(20) COMMENT '日志-最后修改执行人',
// modified_time DATETIME COMMENT '日志-最后修改时间',
Date nowTime=new Date();
user.setIsDelete(0);
user.setCreatedUser(user.getUsername());
user.setCreatedTime(nowTime);
user.setModifiedUser(user.getUsername());
user.setModifiedTime(nowTime);
Integer rows = userDao.insert(user);
if (rows == 0 ){throw new InsertException("注册失败(未知失败)请重新注册");
};
}else {
throw new UsernameOccupyException("用户名被占用");
}
}
@Override
public User login(String username,String password) {
User UserLogin = userDao.findByUserName(username);
//盐值认证
String md5Password = getMD5Password(password, UserLogin.getSalt());
if (UserLogin == null){
throw new UserNullException("用户不存在");
}
//检测密码是否匹配
if (!UserLogin.getPassword().equals(md5Password)){
throw new PasswordNotMatchException("用户密码错误");
}
//判断is_delete字段值是否为1表示被标记为删除
if (UserLogin.getIsDelete() == 1){
throw new UserNullException("用户已被删除");
}
User user = new User();
user.setUid(UserLogin.getUid());
user.setUsername(UserLogin.getUsername());
user.setAvatar(UserLogin.getAvatar());
//返回用户数据,是为了辅助页面
return user;
}
@Override
public void changePassword(Integer uid, String username, String oldPassword, String newPassword) {
User user = userDao.findByUid(uid);
if (user ==null){
throw new UserNullException("用户不存在");
}
if (user.getIsDelete() ==1){
throw new UserDeletedException("用户不存在或已被删除");
}
//密码对比
String md5Password = getMD5Password(oldPassword, user.getSalt());
if (!user.getPassword().equals(md5Password)){
throw new PasswordNotMatchException("原密码错误");
}
//更新password
String newPasswordMd5 = getMD5Password(newPassword, user.getSalt());
Integer rows = userDao.updatePasswordByUid(uid, newPasswordMd5,
username,
new Date());
if (rows !=1){
throw new PasswordUpdateException("修改密码未知异常");
}
}
//根据id获取userInfo
@Override
public User getUserInfoByUid(Integer uid) {
User result = userDao.findByUid(uid);
if (result == null || result.getIsDelete() ==1)
{
throw new UserNullException("用户不存在");
}
User user = new User();
user.setUsername(result.getUsername());
user.setUid(result.getUid());
user.setPhone(result.getPhone());
user.setEmail(result.getEmail());
user.setGender(result.getGender());
return user;
}
//修改用户信息
@Override
public void changeUserInfo(Integer uid, String username, User user) {
User result = userDao.findByUid(uid);
if (result == null || result.getIsDelete() ==1)
{
throw new UserNullException("用户不存在");
}
user.setUid(uid);
user.setModifiedUser(username);
user.setModifiedTime(new Date());
Integer rows = userDao.updateUserInfoByUid(user);
if (rows != 1){
throw new InfoUpdateException("修改用户信息未知异常");
}
}
//password加密方法
private String getMD5Password(String password,String salt){
for (int i =0;i<5;i++){
password = DigestUtils.md5DigestAsHex((salt + password + salt).getBytes()).toUpperCase();
}
//返回加密之后的密码
return password;
}
}
JWT工具类
public class JWTUtil {
private static final String TOKENKey="qgs12345";
/**
* 生成token
* @param map
* @return 返回token
*/
public static String getToken(Map<String,String> map){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7);//7天过期
//添加payload
JWTCreator.Builder builder = JWT.create();
map.forEach((k,v)->{
builder.withClaim(k,v);
});
builder.withExpiresAt(instance.getTime());//设置令牌过期时间
//生成并返回token
return builder.sign(Algorithm.HMAC256(TOKENKey)).toString();
}
/**
* 验证token
* @param token
*/
public static void verify(String token){
JWT.require(Algorithm.HMAC256(TOKENKey)).build().verify(token);
}
/**
* 获取token中payload
* @param token
* @return
*/
public static DecodedJWT getTokenInfo(String token){
return JWT.require(Algorithm.HMAC256(TOKENKey)).build().verify(token);
}
}
UserController
@RestController
@RequestMapping("/users")
@CrossOrigin //表示都允许跨域访问
public class UserController extends BaseController{
@Autowired
private UserService userModuleService;
@RequestMapping("/login")
public Map<String,Object> login(String username, String password){
Map<String,Object> map =new HashMap<>();
try {
User data = userModuleService.login(username, password);
Map<String,String> payload =new HashMap<>();
payload.put("username",data.getUsername());
//生成JWT令牌
String token =JWTUtil.getToken(payload);
map.put("state",true);
map.put("msg","登入成功");
map.put("token",token);
}catch (Exception e){
map.put("state",false);
map.put("msg","登入失败");
}
return map;
}
}
登录,产生token
验证token方式一
认证代码中写token认证流程,过多的认证请求会导致代码冗余
@RestController
@RequestMapping("/users")
@CrossOrigin //表示都允许跨域访问
public class UserController extends BaseController{
@Autowired
private UserService userModuleService;
@RequestMapping("/login")
//验证token
@RequestMapping("/loginVerify")
public Map<String,Object> loginVerify(String token){
System.out.println(token);
Map<String,Object> map =new HashMap<>();
try {
//生成JWT令牌
JWTUtil.verify(token);
map.put("state",true);
map.put("msg","验证成功");
}catch (Exception e){
map.put("state",true);
map.put("msg","验证失败");
}
return map;
}
}
验证token方式二 JWT拦截器
抛弃方式一的代码冗余
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Map<String,Object> map =new HashMap<>();
// 获取请求头中的JWT令牌
String token = request.getHeader("token");
// 进行JWT令牌的验证逻辑
try {
//生成JWT令牌
JWTUtil.verify(token);
return true;//放行请求
}catch (Exception e){
map.put("state",false);
map.put("msg","token验证失败");
//map转json
String msg =new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(msg);
}
return false;
}
}
@Component
public class InterceptorConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/users//loginVerify")//拦截路径,根据实际情况进行配置
.excludePathPatterns("/users/login","/reg") ;//放行路径
}
}
简化loginVerify后
//验证token
@RequestMapping("/loginVerify")
public Map<String,Object> loginVerify(HttpServletRequest request){
Map<String,Object> map =new HashMap<>();
String token =request.getHeader("token");
System.out.println(token);
map.put("state",true);
map.put("msg","验证成功");
return map;
}
请求头携带token效果