环境介绍
技术栈 |
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效果
Session认证
session用户认证流程
1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话( session)里面保存相关数据,比如用户角色登录时间等。
3、服务器向用户返回一个 session_id,写入用户的 Cookie。
4、用户随后的每一次请求,都会通过Cookie,将 session_id传回服务器。
5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。