SpringBoot中集成jwt实现前后端分离的token验证机制

时间:2022-07-24
本文章向大家介绍SpringBoot中集成jwt实现前后端分离的token验证机制,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

01

jwt简介

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。

作为session的替代品,可以很好的应用到前后端分离的项目当中

02

思路

接口设计采用restful风格规范,前后端交互采用json

大体思路为:

用户使用用户名密码或者其他方式验证方式请求服务器

服务器进行验证用户的信息

服务器通过验证发送给用户一个token

客户端存储token,并在每次请求时附送上这个token值

服务端验证token值,并返回数据

03

写写代码

将主要代码进行了整理,稍微有点代码基础的应该都能很好的理解

如果我哪里没写清楚,没看明白的,也可以私信我进行咨询

在pom.xml配置文件中引入依赖

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

编写拦截器配置文件

拦截/mrc下的所有请求,但是对登录不做拦截

@Configuration
public class APIIntercepterConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new APIInterceptor())
                .addPathPatterns("/mrc/**")
                .excludePathPatterns("/mrc/login/**");
super.addInterceptors(registry);
    }
}

编写拦截器

对于拦截的前端请求,从header拿到token

先判断请求的接口方法有没有PassToken注解(后边会讲到,如果方法上配置了这个注解,则直接放行)

然后判断是否有UserLoginToken注解,如果有,则进行token验证

获取到用户信息,如果用户不存在或者token验证没通过,则返回401异常

afterCompletion方法中的一些配置是为了解决跨域存在的一些问题


@Component
public class APIInterceptor implements HandlerInterceptor {
@Autowired
    IUserService userService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
// 从 http 请求头中取出 token
        String token = httpServletRequest.getHeader("token");
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
            }
        }
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(UserLoginToken.class)) {
            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("无token,请重新登录");
                }
// 获取 token 中的 user id
                String userId;
try {
                    userId = JWT.decode(token).getAudience().get(0);
                } catch (JWTDecodeException j) {
throw new RuntimeException("401");
                }
                User user = UserUtils.get(userId);
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
                }
// 验证 token
                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getLoginName())).build();
try {
                    jwtVerifier.verify(token);
                } catch (JWTVerificationException e) {
throw new RuntimeException("401");
                }
return true;
            }
        }
return true;
    }
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        System.out.println("方法处理中==========");
    }
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Headers", "Authorization,Content-Type,X-Requested-With,token");
        response.setHeader("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,POST,PUT,DELETE");
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Max-Age", "3600");
//        System.out.println("方法处理后==========");
    }

生成token的方法

我这里是用用户的loginName做生成token的密钥和签名

配置token的有效时长

@Service
public class TokenService {
public TokenInfo getToken(User user) {
Date start = new Date();
//一小时有效时间
        long expiresIn = 60 * 60 * 1000;
        long currentTime = System.currentTimeMillis() + expiresIn;
Date end = new Date(currentTime);
String token;
//以用户的云信id做唯一标识
        token = JWT.create().withAudience(user.getLoginName()).withIssuedAt(start).withExpiresAt(end)
                .sign(Algorithm.HMAC256(user.getLoginName()));
        TokenInfo tokenInfo = new TokenInfo();
        tokenInfo.setLoginName(user.getLoginName());
        tokenInfo.setToken(token);
        tokenInfo.setExpiresIn(expiresIn);
        tokenInfo.setTokenUpdateTime(start.getTime());
return tokenInfo;
    }
}

跳过验证的注解

作用在接口方法上

/***
 * 用来跳过验证的 PassToken
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}

需要进行token验证的注解

作用在接口方法上

/**
 * 用于登录后才能操作的token
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}

用户登录接口

这里是做的集成到云信(企业微信)的单点登录

在云信平台中,通过点击应用,会重定向到login接口,并传递两个参数code和state

其中code用户从云信(企业微信)获取用户信息

state为自定义的一个字符串,用作后端程序对重定向来源的一个验证

验证没有问题,则生成token,并重定向到前端的地址,并附带用户信息和token

getToken接口用作token的更新

给testToken加上@UserLoginToken注解,用作验证是否生效



@Api("用户登录管理")
@Controller
@RequestMapping("/mrc")
public class LoginAPI {
/**
     * 前端首页地址
     */
public final String MRC_INDEX_URL = "";
public final String MRC_ERROR_URL = "/error";
@Autowired
    IUserService userService;
@Autowired
    TokenService tokenService;
/**
     * 单点登录
     */
@RequestMapping("/login")
public void login(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String code = request.getParameter("code");
        String state = request.getParameter("state");
        System.out.println(code + "===" + state);
if (!YunChatTokenUtils.STATE.equalsIgnoreCase(state) || StringUtils.isEmpty(code)) {
            response.sendRedirect(MRConstant.MARKETING_RESOURCES_CLIENT_URL_HEADER + MRC_ERROR_URL);
        }
//获取用户的基本信息
        JSONObject userJSONObject = OAuth2Utils.getUserInfoByCode(code);
// 获取用户信息成功
if (userJSONObject != null
                && YunChatConstant.ERROR_CODE_SUCCESS == (userJSONObject.getIntValue(OAuth2Utils.KEY_ERRCODE))) {
            User user = OAuth2Utils.getUserByUserTicket(userJSONObject.getString(OAuth2Utils.KEY_USER_TICKET));
        User mrUser = UserUtils.get(user.getLoginName());
if (mrUser == null) {
            userService.insertUser(user);
            mrUser = userService.selectUserByLoginName(user.getLoginName());
            UserUtils.addOrUpdate(mrUser);
        }
        TokenInfo tokenInfo = tokenService.getToken(mrUser);
        response.sendRedirect(MRConstant.MARKETING_RESOURCES_CLIENT_URL_HEADER + MRC_INDEX_URL
                + "?token=" + URLEncoder.encode(JSONObject.toJSONString(tokenInfo)));
        } else {
            response.sendRedirect(MRC_ERROR_URL);
        }
    }
/**
     * test
     * 更新token
     */
@ApiOperation("获取更新token")
@ApiImplicitParam(name = "loginName", value = "用户登录名", required = true, dataType = "String", paramType = "path")
@UserLoginToken
@ResponseBody
@RequestMapping(value = "/token/{loginName}", method = RequestMethod.GET)
public AjaxResult getToken(@PathVariable("loginName") String loginName) {
        User user = UserUtils.get(loginName);
        TokenInfo tokenInfo = tokenService.getToken(user);
if (tokenInfo == null) {
return AjaxResult.error("更新token失败");
        }
        MRTokenVO mrTokenVO = new MRTokenVO();
        mrTokenVO.setExpiresIn(tokenInfo.getExpiresIn());
        mrTokenVO.setToken(tokenInfo.getToken());
        mrTokenVO.setTokenUpdateTime(tokenInfo.getTokenUpdateTime());
        MRUserVO mrUserVO = new MRUserVO(user);
        mrTokenVO.setUser(mrUserVO);
return AjaxResult.apiSuccess(mrTokenVO);
    }
/**
     * test
     * 更新token
     */
@ApiOperation("测试token")
@UserLoginToken
@ResponseBody
@RequestMapping(value = "/token/test/{loginName}", method = RequestMethod.GET)
public AjaxResult testToken(@PathVariable("loginName") String loginName) {
return AjaxResult.apiSuccess("测试成功");
    }
}

文/戴先生@2020年5月30日

---end---