点击登录按钮,后端验证账号密码是否通过,如果通过则生成token,把token发送给前端,前端保存到cookie(前后端分离是不能使用保存session,因为每次发送ajax请求响应后都会断开服务器,就会导致session生命周期就销毁掉,然后再发送请求时再重新连接服务器,session已经不存在了),之后访问受限资源就需要取cookie拿到token,然后作为参数(放在请求头更安全)发送给后端,后端验证token。
Jwt是由三部分组成的字符串(header头部,payload载荷,signature签名)
头部:用于描述关于该 JWT 的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个 JSON 对象。例如: { "typ": "JWT", "alg": "HS256" } 载荷:其实就是自定义的数据,一般存储用户 Id,过期时间等信息。也就是 JWT 的核心所在,因为这些数据就是使后端知道此 token 是哪个用户已经登录的凭证。而且这些数据是存在 token 里面的,由前端携带,所以后端几乎不需要保存任何数据。 例如: { "uid": "xxxxidid", //用户id "exp": "12121212" //过期时间 } 签名:1.头部和载荷 各自base64加密后用.连接起来,然后就形成了 xxx.xx 的前两段 token。2.最后一段 token 的形成是,前两段加入一个密匙用 HS256 算法或者其他算法加密形成。3. 所以 token3 段的形成就是在签名处形成的。
将这三部分用.连接成一个完整的字符串,构成了最终的jwt
一、添加依赖
//提供JWT的java类
com.auth0
java-jwt
3.10.3
//提供JWT算法
io.jsonwebtoken
jjwt
0.9.1
二、给登录按钮添加单击事件@click="doSubmit"。给账号文本框密码文本框都添加@blur="checkInfo"事件。发送请求,接收响应信息
重点代码: setCookieValue("token",vo.msg) 调用自定义方法setCookieValue把token放到cookie
三、Controller接收请求
@GetMapping("/user/login")
public ResultVO login(@RequestParam("username") String name,
@RequestParam("password") String pwd){
return userService.checkLogin(name,pwd);
}
四、Service和ServiceImpl处理业务、生成token
public interface UserService {
public ResultVO checkLogin(String name, String pwd);
}
@Service
@Scope("singleton")//singleton单例模式,全局有且仅有一个实例
public class UserServiceImpl implements UserService {
@Resource
private UsersMapper usersMapper;
@Override
public ResultVO checkLogin(String name, String pwd) {
//使用tkmapper查询user 你们用自己写的mapper也可以
Example example = new Example(Users.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("username",name);
List users = usersMapper.selectByExample(example);
if(users.size()==0){
return new ResultVO(10001,"登录失败,用户名不存在",null);
}else {
Users user = users.get(0);
String md5Pwd = MD5Utils.md5(pwd);
// 数据库密码是MD5加密过的,MD5算法又不可逆,所以只能把登录密码加密后去和数据库的密码比对
if(user.getPassword().equals(md5Pwd)){
//如果登录成功,生成token
JwtBuilder builder = Jwts.builder();
HashMap map = new HashMap<>();
map.put("key1","value1");
map.put("key2","value2");
String token = builder.setSubject(name) //载荷部分,主题,就是token中携带的数据,这里把用户名放进去
.setIssuedAt(new Date()) //设置token的生成时间
.setId(users.get(0).getUserId() + "") //设置用户id为token id ''是因为用户id是int类型,需要转换为字符串类型
.setClaims(map) //map中可以存放用户的角色权限信息
.setExpiration(new Date(System.currentTimeMillis() + 24*60*60*1000)) //设置token过期时间,当前时间加一天就是时效为一天过期
.signWith(SignatureAlgorithm.HS256, "ycj123456") //签名部分,设置HS256加密方式和加密密码,ycj123456是自定义的密码
.compact();
return new ResultVO(10000,token,user);//把token封装到ResultVO传到前端。
}else {
return new ResultVO(10001,"密码错误",null);
}
}
}
}
五、MD5、返回的工具类
package com.ycj.utils;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
//MD5 生成器
public class MD5Utils {
public static String md5(String password){
//生成一个md5加密器
try {
MessageDigest md = MessageDigest.getInstance("MD5");
//计算MD5 的值
md.update(password.getBytes());
//BigInteger 将8位的字符串 转成16位的字符串 得到的字符串形式是哈希码值
//BigInteger(参数1,参数2) 参数1 是 1为正数 0为零 -1为负数
return new BigInteger(1, md.digest()).toString(16);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
}
public class ResStatus {
public static final int OK=10000;
public static final int NO=10001;
public static final int LOGIN_SUCCESS = 2000; //认证成功
public static final int LOGIN_FAIL_NOT = 20001; //用户未登录
public static final int LOGIN_FAIL_OVERDUE = 20002; //用户登录失效
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultVO {
private int code;
private String msg;
private Object data;
}
六、Dao查询用户信息
这里是使用tkMapper,selectByExample()就是它提供的,你们也可以自己写mapper
public interface UsersMapper extends Mapper,MySqlMapper {
//继承了Mapper,MySqlMapper就不用写dao和mapper了
}
七、从cookie中存取token
//cookie只能存放键值对
var operator = "=";
//window.document.cookie可以拿到cookie所有的key=value;形式的字符串。所以从cookie拿值,遍历cookie的所有key,直到key等于keyStr,
//就可以拿到对应的值,例如我们要拿名为token的key,调用方法getCookieValue(token)就可以拿到key为token的值(value)
function getCookieValue(keyStr){
var value = null;
var s = window.document.cookie;
var arr = s.split("; ");
for(var i=0; i
八、携带token访问受限资源示例,发送请求,接收响应信息
重点代码:var token = getCookieValue("token"); headers:{token:token //访问受限资源必须把token传到后端校验},
九、设置拦截器
如果受限资源有多个,我们可以设置拦截器去校验token,就不用每次都去校验一次
配置拦截路径
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private CheckTokenInterceptor checkTokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(checkTokenInterceptor)
.addPathPatterns("/shopcart/**")
.addPathPatterns("/orders/**")
.addPathPatterns("/useraddr/**")
.addPathPatterns("/user/check");
}
拦截并解析token
@Component
public class CheckTokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//关于浏览器的请求预检.在跨域的情况下,非简单请求会先发起一次空body的OPTIONS请求,称为"预检"请求,用于向服务器请求权限信息,等预检请求被成功响应后,才发起真正的http请求。
String method = request.getMethod();
if("OPTIONS".equalsIgnoreCase(method)){
return true;
}
// String token = request.getParameter("token");放入params才能用这个,放hearder用getHearder
String token = request.getHeader("token");
if(token == null){
ResultVO resultVO = new ResultVO(ResStatus.LOGIN_FAIL_NOT, "请先登录!", null);
doResponse(response,resultVO);
}else{
try {
JwtParser parser = Jwts.parser();
parser.setSigningKey("ycj123456"); //解析token的SigningKey必须和生成token时设置密码一致
//如果token检验通过(密码正确,有效期内)则正常执行,否则抛出异常
Jws claimsJws = parser.parseClaimsJws(token);
return true;//true就是验证通过,放行
}catch (ExpiredJwtException e){
ResultVO resultVO = new ResultVO(ResStatus.LOGIN_FAIL_OVERDUE, "登录过期,请重新登录!", null);
doResponse(response,resultVO);
}catch (UnsupportedJwtException e){
ResultVO resultVO = new ResultVO(ResStatus.LOGIN_FAIL_NOT, "Token不合法,请自重!", null);
doResponse(response,resultVO);
}catch (Exception e){
ResultVO resultVO = new ResultVO(ResStatus.LOGIN_FAIL_NOT, "请先登录!", null);
doResponse(response,resultVO);
}
}
return false;
}
//没带token或者检验失败响应给前端
private void doResponse(HttpServletResponse response,ResultVO resultVO) throws IOException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
PrintWriter out = response.getWriter();
String s = new ObjectMapper().writeValueAsString(resultVO);
out.print(s);
out.flush();
out.close();
}
}
如果token校验通过,返回true,拦截器会放行,可以访问受限资源。否则,使用doResponse(response,resultVO)响应信息给前端页面。