springboot集成JWT 登录验证

前言

 嗯,给之前的日志功能界面加登录验证。

一、集成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的话,拦截校验当然放在网关统一校验,校验通过把账号添加到请求地址上。

附件

springboot+jwt登录认证+实时日志显示-Java文档类资源-CSDN下载

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值