01-前后端分离的登录功能
-
概述
- 前后端分离的项目中的用户登录是无状态,也就是无法使用session来记录用户登录状态,可以使用token令牌机制来处理。
-
登录流程
-
令牌流程
- 用户提交账户和密码到服务器的认证(登录)接口
- 认证接口
- 接收账户和密码进行认证
- 认证通过生成token
- 如果是随机token则需要在服务器进行存储
- 如果是按照特定的协议生成则无需存储
- 将生成的token响应给前端
- 前端获取并存储token(cookie、localstorage)
- 当前端再次请求服务器接口时必须携带token(header)
02-登录功能实现
- 代码实现
- login.html
- 发起登录请求
- 保存token令牌
<body> <div id="app"> <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px"> <el-form-item label="账户" prop="username"> <el-input v-model="ruleForm.username"></el-input> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="ruleForm.password"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm('ruleForm')">登录</el-button> </el-form-item> </el-form> </div> <script> new Vue({ el: "#app", data: { ruleForm: { username: "tianqi", password: "123456" }, rules: { username: [ {required: true, message: '请输入账户', trigger: 'blur'}, {min: 3, message: '最少3个字符', trigger: 'blur'} ], password: [ {required: true, message: '请输入密码', trigger: 'blur'}, {min: 3, message: '最少3个字符', trigger: 'blur'} ] } }, methods: { submitForm(formName) { var _this = this; console.log("username : " + this.ruleForm.username); this.$refs[formName].validate((valid) => { if (valid) { axios({ method:"get", url:"http://localhost:8080/restful01/user/login", params:{ "username": _this.ruleForm.username, "password": _this.ruleForm.password } }).then(function (res) { //获取后台传递的token令牌 if (res.data.code == 1) { var token = res.data.t; console.log(token); document.cookie = "token="+token; console.log("将token设置到cookie中 " + document.cookie); if (res.data.code == 1 ){ location.href = "index.html"; } } }); } else { console.log('error submit!!'); return false; } }); } } }); </script> </body>
- TokenUtil.java
- 生成token的工具类
public class TokenUtil { public static String getToken(String username){ String f = username.substring(0,1); String l = username.substring(username.length()-1); String str =f + l + username + f+ l ; System.out.println(str); String token = username + "-" + Base64.encode(str.getBytes()).replaceAll("=","a"); return token; } public static void main(String[] args) { System.out.println(getToken("tianqi")); } }
- UserController.login方法
- 完成认证,生成token
@ApiOperation(value = "用户登录") @RequestMapping(value="/login",method = RequestMethod.GET) public ResultVO login( String username, String password) throws Exception { User user = userService.login(username,password); Integer code = 0; String msg = "登录成功"; String token = null; if (null != user) { code = 1; msg = "登录成功"; //获取token令牌 token = TokenUtil.getToken(username); } else { code = 0; msg = "登录失败"; } return new ResultVO(code ,msg,token); }
- login.html
03-认证功能实现
-
概述
- 当前端再次请求服务器接口时必须携带token(使用header请求头携带token)
-
代码实现
getUserListByPage(){ console.log(document.cookie); var token ; var cookies = document.cookie.split(","); for (let i = 0; i < cookies.length; i++) { var cookie = cookies[i]; var cookieStr = cookie.split("="); if ("token" == cookieStr[0]){ token = cookieStr[1]; } } var _this = this; axios({ method:"get", url:"http://localhost:8080/restful01/user/getUserListByPage", params: { currentPage:_this.pageInfo.currentPage, pageSize:_this.pageInfo.pageSize }, headers:{ "token":token } }).then(function (res) { var data = res.data; console.log(res.data); if (0 == data.code) { // location.href = "test.html"; top.location = "test.html"; } else { _this.tableData = data.t.list; _this.pageInfo.currentPage = data.t.pageNum; _this.pageInfo.pageSize= data.t.pageSize; _this.pageInfo.total = data.t.total; }
});
}
* IllegalTokenException.java * 自定义异常
public class IllegalTokenException extends Exception {
public IllegalTokenException() { } public IllegalTokenException(String message) { super(message); } public IllegalTokenException(String message, Throwable cause) { super(message, cause); } public IllegalTokenException(Throwable cause) { super(cause); } public IllegalTokenException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); }
}
* LoginInterceptor.java * 完成token认证 ```java public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("options".equalsIgnoreCase(request.getMethod())) { return true; } else { String token = request.getHeader("token"); String username = token.split("-")[0]; String existToken = TokenUtil.getToken(username); if (token.equals(existToken) ) { return true; } else { throw new IllegalTokenException("非法token,请验证!"); } } } }
- GlobalExceptionResolver.java
- 全局异常处理期
@ControllerAdvice public class GlobalExceptionResolver { @ResponseBody @ExceptionHandler(IllegalTokenException.class) public ResultVO handleException(Exception e){ return new ResultVO(0,e.getMessage()); } }
- GlobalExceptionResolver.java
-
存在的问题
- 无法设置token的有效时长
04-JWT
- 概述
- Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
- JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
- 组成
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oiyMg4wt-1599120860892)(https://note.youdao.com/yws/res/24864/6F468A6D715447428B4E41DE0DA654BF)]
- header:类型及算法
- payload: 载荷就是存放有效信息的地方
- signature: 签证信息
05-认证引入token
-
开发步骤
- 引入依赖
- 改造TokenUtil,生成token
- 拦截器校验JWT token
- 改造拦截器LoginInterceptor
- 改造全局异常处理期
-
代码实现
- 引入依赖
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.3</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency
- 改造TokenUtil,生成token
public class TokenUtil {
public static String getToken(User user){ String token = Jwts.builder() .setSubject(user.getUsername()) .setId(user.getId()+"") .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 60 * 1000)) .compact(); return token; }
}
* 改造拦截器LoginInterceptor ```java @Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws UnTokenException, ExpiredJwtException,SignatureException { if("options".equalsIgnoreCase(request.getMethod())){ return true; }else { String token = request.getHeader("token"); System.out.println(token); if (token != null && !"".equals(token)) { //校验token JwtParser parser = Jwts.parser(); parser.setSigningKey("qianfeng"); //解析token,只要不抛出异常表示token正常 Jws<Claims> claimsJws = parser.parseClaimsJws(token); //从token中获取信息 Claims body = claimsJws.getBody(); String subject = body.getSubject(); return true; } else { throw new UnTokenException("token为NULL"); } } } }
- 改造全局异常处理期GlobalExceptionResolver
@ControllerAdvice public class GlobalExceptionResolver { @ResponseBody @ExceptionHandler(JwtException.class) public ResultVO handleException(Exception e){ return new ResultVO(0,e.getMessage()); } }