该脚手架基于人人开源项目renren-security改造,包含相关框架的集成过程以及配置的相关解读。
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。 流程上是这样的: 用户使用用户名密码来请求服务器 服务器进行验证用户的信息 服务器通过验证发送给用户一个token 客户端存储token,并在每次请求时附送上这个token值 服务端验证token值,并返回数据 这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin:*
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了) InterceptorConfig.javajwt的拦截配置
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 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,如下所示:
jwt是无状态的,也就是说使用jwt进行登陆验证并不能实现单点登陆,同一用户允许同一时间在不同地点登陆。 具体的springboot+jwt集成请参考:SpringBoot集成JWT实现token验证
下一个shiro,未完待续…