前言
嗯,给之前的日志功能界面加登录验证。
一、集成JWT
maven配置
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sakyoka.test</groupId>
<artifactId>springboot-websocket-log-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot-websocket-log-test</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- springboot start -->
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- <exclusions> <exclusion> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- socket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- springboot end -->
<!-- utils start -->
<!-- 日志 logging-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- utlis end -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.1.19</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
</dependencies>
<build>
<finalName>springboot-websocket-log-test</finalName>
<resources>
<resource>
<directory>${basedir}/src/main/webapp</directory>
<!--注意此次必须要放在此目录下才能被访问到-->
<targetPath>META-INF/resources</targetPath>
<includes>
<include>**/**</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
<includes>
<include>**/**</include>
</includes>
</resource>
</resources>
</build>
</project>
JwtTokenUtils 封装jwt生成token和校验token值,可以设置token有效时间,这里设置半个小时。
package com.sakyoka.test.utils;
import java.util.Date;
import java.util.UUID;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
/**
*
* 描述:jwt token生成、校验
* @author sakyoka
* @date 2022年8月31日 下午5:20:35
*/
public class JwtTokenUtils {
private JwtTokenUtils(){}
/**有效时长:默认半个小时*/
private static Long EXPIRATION_DATE =
Long.valueOf(Properties.get("login.jwt.expiration_date", Long.valueOf(30 * 60 * 1000).toString()));
/**token秘钥*/
private static String TOKEN_SECRET = Properties.get("login.jwt.token_secret", "123");
/**签发主体*/
private static String TOKEN_USER = Properties.get("login.jwt.token_user", "sakyoka");
/**
*
* 描述:生成token字符串
* @author sakyoka
* @date 2022年8月31日 下午5:20:12
* @param userName
* @return String
*/
public static String tokenCreate(String userName){
String token = JWT.create()
.withJWTId(UUID.randomUUID().toString().replace("-", ""))
.withIssuer(TOKEN_USER)
.withClaim("username", userName)
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_DATE))
.sign(Algorithm.HMAC256(TOKEN_SECRET));
return token;
}
/**
*
* 描述:token校验
* @author sakyoka
* @date 2022年8月31日 下午5:21:41
* @param token
* @return true有效,false无效
*/
public static boolean tokenValid(String token){
try {
DecodedJWT decodedJWT = getJwt(token);
Claim userClaim = decodedJWT.getClaim("username");
if (userClaim.isNull()){
System.out.println("token get username is null");
return false;
}
//String userName = userClaim.asString();
Date expirationDate = decodedJWT.getExpiresAt();
return System.currentTimeMillis() < expirationDate.getTime();
} catch (Exception e) {
e.getStackTrace();
return false;
}
}
/**
*
* 描述:获取DecodedJWT
* @author sakyoka
* @date 2022年9月1日 上午10:01:33
* @param token
* @return DecodedJWT
*/
private static DecodedJWT getJwt(String token){
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET))
.withIssuer(TOKEN_USER).build();
DecodedJWT decodedJWT = verifier.verify(token);
return decodedJWT;
}
}
二、配置拦截器
自定义CommonInterceptor拦截器,实现HandlerInterceptor的接口,主要校验header或者url上的token是否为空,是否有效。无效设置401并且跳转到登录页面
package com.sakyoka.test.systemconfig.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.sakyoka.test.utils.JwtTokenUtils;
import lombok.extern.log4j.Log4j;
/**
*
* 描述:请求拦截器
* @author sakyoka
* @date 2022年9月2日 下午1:54:41
*/
@Component
@Log4j
public class CommonInterceptor implements HandlerInterceptor{
private static final int UNAUTHORIZED = 401;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//从header中获取token值
String token = request.getHeader("token");
token = StringUtils.isBlank(token) ? request.getParameter("token") : token;
if (StringUtils.isBlank(token)){
log.debug("header token is null");
response.setStatus(UNAUTHORIZED);
response.sendRedirect("/login");
return false;
}
//判断token有效性
boolean tokenValid = JwtTokenUtils.tokenValid(token);
if (!tokenValid){
log.debug("invalid token");
response.sendRedirect("/login");
response.setStatus(UNAUTHORIZED);
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)throws Exception {
}
}
注册CommonInterceptor拦截器,并且添加要拦截的地址
package com.sakyoka.test.systemconfig.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.sakyoka.test.systemconfig.interceptor.CommonInterceptor;
/**
*
* 描述:web配置
* @author sakyoka
* @date 2022年9月2日 下午1:59:04
*/
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
CommonInterceptor commonInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截所有/log 开头接口
registry.addInterceptor(commonInterceptor)
.addPathPatterns("/log/**");
}
}
三、编写登录方法、登录页面
登录类,使用本地文件+配置文件的数据测试登录账号,这里就不连接数据校验了
package com.sakyoka.test.login.controller;
import java.io.File;
import org.apache.catalina.servlet4preview.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sakyoka.test.systemconfig.exception.CommonException;
import com.sakyoka.test.systemconfig.result.ResultBean;
import com.sakyoka.test.utils.FileUtils;
import com.sakyoka.test.utils.JwtTokenUtils;
import com.sakyoka.test.utils.OsUtils;
import com.sakyoka.test.utils.Properties;
import lombok.extern.log4j.Log4j;
/**
*
* 描述:登录控制层
* @author sakyoka
* @date 2022年9月2日 下午2:02:49
*/
@Log4j
@Controller
public class LoginController {
/**用户存储文件*/
private static String LOGIN_USERS_FILE;
/**默认账号*/
private static final String DEFAULT_USER_NAME = Properties.get("login.default.username");
/**默认密码*/
private static final String DEFAULT_PASSWORD = Properties.get("login.default.password");
static{
String rootPath = OsUtils.isWindow() ? "D:\\" : "/mnt/";
String userPath = rootPath + File.separator + "users_data";
LOGIN_USERS_FILE = userPath + File.separator + "users.json";
FileUtils.ifNotExistsCreate(LOGIN_USERS_FILE);
}
/**
*
* 描述:登录页面
* @author sakyoka
* @date 2022年8月31日 下午3:06:59
* @return ModelAndView
*/
@GetMapping("/login")
public ModelAndView loginpage(){
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("/login/login");
return modelAndView;
}
/**
*
* 描述:登录方法
* @author sakyoka
* @date 2022年8月31日 下午3:08:15
* @return ResultBean
*/
@PostMapping("/login")
@ResponseBody
public ResultBean login(HttpServletRequest request){
String username = request.getParameter("username");
if (StringUtils.isBlank(username)){
return ResultBean.badRequest("username is blank", null);
}
String password = request.getParameter("password");
if (StringUtils.isBlank(password)){
return ResultBean.badRequest("password is blank", null);
}
String token = JwtTokenUtils.tokenCreate(username);
//校验账号密码,有数据库当然读取数据库,这里直接读取本地文件
//校验通过生成token
String userContect = FileUtils.read(LOGIN_USERS_FILE);
if (StringUtils.isNotBlank(userContect)){
JSONObject jsonObject = JSON.parseObject(userContect);
if (!jsonObject.containsKey(username)){
if (this.checkDefaultUser(username, password)){
return ResultBean.ok(token);
}
}
String _password = jsonObject.getJSONObject(username).getString("password");
if (password.equals(_password)){
return ResultBean.ok(token);
}
}else{
if (this.checkDefaultUser(username, password)){
return ResultBean.ok(token);
}
}
return ResultBean.builder()
.code(ResultBean.UNAUTHORIZED)
.msg("校验失败")
.build();
}
/**
*
* 描述:检查默认账号
* @author sakyoka
* @date 2022年8月31日 下午6:16:17
* @param username
* @param password
* @return true通过,false不通过
*/
private boolean checkDefaultUser(String username, String password){
if (StringUtils.isBlank(DEFAULT_USER_NAME)) {
log.warn("没有设置默认账号,请在配置文件设置login.default.username、login.default.password");
throw new CommonException("没有设置默认账号,请在配置文件设置login.default.username、login.default.password");
}
if (DEFAULT_USER_NAME.equals(username)){
if (DEFAULT_PASSWORD.equals(password)){
return true;
}else{
throw new CommonException("password incorrect");
}
}
throw new CommonException("not match account");
}
/**
*
* 描述:登出方法
* @author sakyoka
* @date 2022年8月31日 下午3:08:22
* @param token
* @return ResultBean
*/
@PostMapping("/loginout")
@ResponseBody
public ResultBean loginout(HttpServletRequest request){
//配合redis?
return ResultBean.ok("");
}
}
登录页面,返回token成功之后,携带token去访问其他页面接口(/log/** 下的接口和页面,因为拦截器只配置了这个路径)
<%@ page contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>login page</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="renderer" content="webkit">
<jsp:include page="/WEB-INF/views/common/commonstatic.jsp" flush="true" />
</head>
<body>
<div id="fisrt-div" style="width: 100%; height: 100%; text-align: center; padding: 10%;">
<div id="loginDiv" style="width: 800px; height: 400px; margin: auto; text-align: center; border: 1px solid #eee; border-radius: 5px; background-color: white; padding: 90px;">
<div style="height: 20%; font-weight: bolder; font-size: 21px; font-family: NSimSun;">login</div>
<div style="height: 70%;">
<table border="0px" style="margin: auto;">
<tbody>
<tr style="height: 40px;">
<td class="first-cols"><font color="red">*</font>账号:</td>
<td><input type="text" name="username" id="username" class="param-field input-class form-control"
valids="notBlank" fieldDesc="账号名称"/></td>
</tr>
<tr style="height: 40px;">
<td class="first-cols"><font color="red">*</font>密码:</td>
<td><input type="password" name="password" id="password" class="param-field input-class form-control"
valids="notBlank" fieldDesc="密码"/></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2" style="text-align: center;">
<button class="btn btn-primary" onclick="login();" style="margin-top: 10px;">登录</button>
<button class="btn btn-default" onclick="reset();" style="margin-top: 10px;">重置</button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</body>
<script type="text/javascript" src="${root}/common/ParamObjectUtils.js"></script>
<script type="text/javascript" src="${root}/common/ValidUtils.js"></script>
<script type="text/javascript" src="${root}/common/URLBuilder.js"></script>
<script>
function login(){
//password 加密?
var loginUrl = root + "/login";
var validResult = ParamObjectUtils.valid();
if (validResult.result === false){
AlertMessgaeUtils.alert({id:'tip', title:'提示', content: validResult.msg});
return ;
}
var paramObject = validResult.data
$.ajax({
url: loginUrl,
type: "post",
data: paramObject,
success: function(res) {
if (res.code == 200){
var token = res.data;
localStorage.setItem("token", token);
location.href = root + "/log/logconsole?token=" + token;
}else{
AlertMessgaeUtils.alert({id:'tip', title:'提示', content: "return msg:" + res.msg});
}
},
error: function(xhr, ts, et){
var status = xhr.status;
var msg = '';
switch (status){
case 400: msg = '请求参数存在错误'; break;
case 401: msg = '请求地址没有权限'; break;
case 403: msg = '请求地址没有权限'; break;
case 404: msg = '未知请求地址'; break;
case 500: msg = '内部服务器处理异常'; break;
default: msg = '请求失败'; break;
}
AlertMessgaeUtils.alert({id:'tip', title:'提示', content: 'msg:' + msg});
}
});
}
</script>
</html>
ok,可以测试了。
四、测试
访问地址http://127.0.0.1:9000/log/logconsole,立刻被拦截转跳到登录页面
用配置文件配置的账号密码登录
进来了。
如果把地址上的token删除,立刻又回到登录页面。
总结
1、测试过程可以发现,登出方法没有实现,确实没有实现。由于jwt产生的token是没有状态
的,设置不了过期没有做。当然,登出还是可以做的,只要做一个黑名单存储起来,单机本地缓存可以,集群用redis。
2、是不是发现,没有token的续期,每过30分钟就得重新登录。这个是有方案的,配合refresh_token。后面做例子补上,这次就简单的token校验。
3、如果有网关springboot gateway的话,拦截校验当然放在网关统一校验,校验通过把账号添加到请求地址上。