springboot集成jwt的登陆验证(token令牌)

前言

该脚手架基于人人开源项目renren-security改造,包含相关框架的集成过程以及配置的相关解读。

JWT的配置与使用

JWT(Json Web Token):基于token的认证方式。

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程上是这样的: 用户使用用户名密码来请求服务器 服务器进行验证用户的信息 服务器通过验证发送给用户一个token
客户端存储token,并在每次请求时附送上这个token值 服务端验证token值,并返回数据
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里,
另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin:*

JWT所需要的pom引入

        <dependency>
		    <groupId>com.auth0</groupId>
		    <artifactId>java-jwt</artifactId>
		    <version>3.4.0</version>
		</dependency>

JWT配置

JWT配置文件由3个文件组成,分别是:AuthenticationInterceptor.java,InterceptorConfig.java,TokenUtils.java。
文件目录在/commont/config/jwt下
  1. AuthenticationInterceptor.java

JWT的验证过程

package io.renren.common.config.jwt;

import java.io.OutputStreamWriter;
import java.util.Date;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Clock;
import com.baomidou.mybatisplus.extension.api.R;

import io.renren.common.utils.Constant;
import io.renren.modules.test.dao.UserDao;
import io.renren.modules.test.entity.UserEntity;

public class AuthenticationInterceptor implements HandlerInterceptor {
	
	@Autowired
    private UserDao userDao;
    
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
    	//踩坑1
    	//浏览器在发送正式的请求时会先发送options类型的请求试探
    	//放行该请求
    	if(httpServletRequest.getMethod().equalsIgnoreCase("OPTIONS")) {
    		return true;
    	}   
    	//踩坑2	
    	//设置允许跨域访问
    	//jwt要设置跨域,不然拿不到请求头里的token
    	httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
    	httpServletResponse.setHeader("Access-Control-Allow-Methods", "*");
    	httpServletResponse.setHeader("Access-Control-Max-Age", "3600");  
    	httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Authorization,"
                + " Content-Type, Accept, Connection, User-Agent, Cookie,token");
    	
    	ServletOutputStream out=httpServletResponse.getOutputStream();
    	OutputStreamWriter ow=new OutputStreamWriter(out,"UTF-8");
    	String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
                // 执行认证   这里的认证改成自己本地的
                if (token == null) {
                	R<Object> r=new R<Object>();
                	r.setCode(401);
                	r.setMsg("请登录后访问");
                	ow.write(JSONObject.toJSONString(r));
                	ow.flush();
                	ow.close();
                	System.out.println("请登录后访问!");              	
                	return false;
                }
                // 获取 token 中的 username
                String username = "";
                try {
                    username = JWT.decode(token).getAudience().get(0);
                } catch (JWTDecodeException j) {//抛异常,因为jwt找不到这个令牌,token失效了,需要重新签发token
                	R<Object> r=new R<Object>();
                	r.setCode(401);
                	r.setMsg("401,请重新登陆!");
                	ow.write(JSONObject.toJSONString(r));
                	ow.flush();
                	ow.close();
                	System.out.println("401");
                	return false;
                }
                UserEntity user=userDao.selectByUsername(username);
                if (user == null) {
                	R<Object> r=new R<Object>();
                	r.setCode(401);
                	r.setMsg("用户不存在,请重新登录");
                	ow.write(JSONObject.toJSONString(r));
                	ow.flush();
                	ow.close();
                	System.out.println("用户不存在,请重新登录");
                	return false;
                }
                // 验证 token
                Clock clock = new Clock() {
                    @Override
                    public Date getToday() {
                        return new Date();
                    }
                };//Must implement Clock interface
//                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(Salt.salt+user.getPassword())).build();//不带超时的token验证方式
                JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWT.require(Algorithm.HMAC256(Constant.JWT_SALT+user.getPassword()));
                JWTVerifier jwtVerifier = verification.build(clock);
                try {
                    jwtVerifier.verify(token);	
                } catch (JWTVerificationException e) {
                	R<Object> r=new R<Object>();
                	r.setCode(401);
                	r.setMsg("登陆超时,请重新登陆!");
                	ow.write(JSONObject.toJSONString(r));
                    //throw new RuntimeException("401");
                	ow.flush();
                	ow.close();
                	System.out.println("token过期,请重新登陆!");
                    return false;
                }
                return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, 
                                  HttpServletResponse httpServletResponse, 
                            Object o, ModelAndView modelAndView) throws Exception {

    }
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, 
                                          HttpServletResponse httpServletResponse, 
                                          Object o, Exception e) throws Exception {
    }
}

这里有两个坑要注意(解决办法在上面的代码中):

 - 放行所有的"OPTIONS"类型的请求(浏览器发起请求时会先发送一个options的试探请求)
 - jwt要设置跨域(不然取不到请求头中的token,不要以为拦截器配置了全局跨域或者加上了@CrossOrgin就ok了)
  1. InterceptorConfig.java

jwt的拦截配置

package io.renren.common.config.jwt;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
	@Bean
	public AuthenticationInterceptor authenticationInterceptor() {
		return new AuthenticationInterceptor();
	}
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
//                .addPathPatterns("/**").excludePathPatterns("/login/**","/v2/**","/swagger-ui.html","/api/**","/login.html","favicon.ico"); 
        .addPathPatterns("/**")//拦截所有路径下的请求(任何请求都需要进行token令牌验证)
        .excludePathPatterns("/login/**");//放行的请求(可以多个逗号隔开,表示这里的请求可以跳过jwt的token令牌验证)  
//        .addPathPatterns("/**").excludePathPatterns("/**");  
    }
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
                .maxAge(3600 * 24);
    }

}

这里有一个值得注意的地方,.addPathPatterns()方法表示拦截的请求范围,而.excludePathPatterns()表示放行的请求,一般情况下只会对登陆接口或者注册接口放行。这样也导致了静态资源如果不登陆就访问不了,比较明显的例子就是swagger接口文档的相关静态资源如:js,css访问不了,因为这个页面在开发过程中是不需要登陆的,目前还没去研究这一方面,我现在的解决方式就是在开发联调需要使用swagger时,将拦截设置为全部开放:.addPathPatterns("/**").excludePathPatterns("/**")
4. TokenUtils.java

JWT token的生成

package io.renren.common.config.jwt;

import java.util.Calendar;
import java.util.Date;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import io.renren.common.utils.Constant;
import io.renren.modules.test.entity.UserEntity;
/**未使用,具体实现在userservice中*/
public class TokenUtils {
	
	public static String getToken(UserEntity user) {
		 String token="";
	     Calendar calendar = Calendar.getInstance();
//	     calendar.add(Calendar.HOUR,2); //特定时间过期,这里设置为2小时之后过期
	     calendar.add(Calendar.HOUR,Constant.JWT_TIMEOUT);
	     Date date = calendar.getTime();
	     token= JWT.create().withAudience(user.getUsername())
	    		 .withExpiresAt(date)//如果不想设置过期时间,把这段注释掉就好,然后修改AuthenticationInterceptor.java中的jwtVerifier 不带超时的token验证方式
	             .sign(Algorithm.HMAC256(Constant.JWT_SALT+user.getPassword()));
	     return token;
    }
}

Constant就是一些常量参数的配置
Constant.JWT_TIMEOUT=数字,
Constant.JWT_SALT=“自定义字符串”,生成token令牌时的加密盐

    /**
     * JWT token加密盐
     * */
    public static final String JWT_SALT = "asd_(8sadm|;.'";
    /**
     * JWT token过期时间(小时)
     * */
    public static final int JWT_TIMEOUT = 4;

JWT的使用

流程:
登陆接口:用户登陆–>账号密码无误–>生成并返回jwt token给前端
其他接口:前端在请求头中携带token,key-value的方式–>后端进行jwt拦截器验证–>token是否存在/用户 信息是否正确/是否超时–>登陆验证通过,可以请求接口数据
使用示例:

	@ResponseBody
	@RequestMapping(value = "/login", method = RequestMethod.POST)
	public R login(@RequestBody UserEntity user) {
		String username=user.getUsername();
		String password=user.getPassword();
		//在这写自己的登陆验证直接数据库核对账号密码,我用的是shiro
		//System.out.println("login:"+username);
		//try{
			//Subject subject = ShiroUtils.getSubject();
		//	UsernamePasswordToken token = new UsernamePasswordToken(username, password);
			//subject.login(token);
		//}catch (UnknownAccountException e) {
		//	return R.error(e.getMessage());
		//}catch (IncorrectCredentialsException e) {
			//return R.error("账号或密码不正确");
		//}catch (LockedAccountException e) {
			//return R.error("账号已被锁定,请联系管理员");
		//}catch (AuthenticationException e) {
		//	return R.error("账户验证失败");
		//}   
		//返回jwt生成的token
		String token = TokenUtils.getToken(user);
        return R.ok().put("token", token);
	}

请求接口数据时需要在请求头带上token,如下所示:
请求接口数据时需要在请求头带上token

JWT最后要说的话

jwt是无状态的,也就是说使用jwt进行登陆验证并不能实现单点登陆,同一用户允许同一时间在不同地点登陆。
具体的springboot+jwt集成请参考:SpringBoot集成JWT实现token验证


下一个shiro,未完待续…

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值