文章目录
- 1.知识准备
- 2. 项目背景:
- 3.开发步骤
- 1.JWT 配置
- 2.拦截器配置,拦截请求获取token
- 3.全局异常及自定义异常配置
- 4.微信小程序端处理异常
- 5. Nginx 配置 支持ssl 登录
- 4.小结:
1.知识准备
1.默认已熟悉SpringBoot2 .
2.Maven 相关知识。
3.JWT相关知识
2. 项目背景:
前后端分离项目,后端采用SpringBoo2.3.12.RELEASE ,
前端微信小程序,实现小程序登录后,后端通过jwt 生成token 返回微信小程序端存储到小程序本地存储。下次再请求携带token, 后台校验token 是否存在,及token 是否过期,后台根据token 解析出携带的用户系统,再从缓存或数据库查询用户信息是否存在,如果不存在或超期,则抛出自定义异常,返回给小程序端,小程序端根据异常状态码判断异常类型,如果是登录异常则跳到登录页重新登录。
下次请求携带
3.开发步骤
1.JWT 配置
- 引入 jjwt maven 依赖
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
- 创建ITokenService 接口及实现类,实现token 的生成、获取token 注册信息,判断是否过期
- ITokenService 接口配置
package com.dechnic.admin.service.jwt;
import io.jsonwebtoken.Claims;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.Map;
/**
* @className: ITokenService
* @author: houqd
* @date: 2022/7/15
**/
public interface ITokenService {
/**
* 根据 身份标识 ,及负载创建token
* @param identityId
* @param claims
* @return
*/
String createToken(String identityId, Map<String,Object> claims);
/**
* 获取token 中注册信息
* @param token
* @return
*/
Claims getTokenClaim(String token);
/**
* Token 是否过期
* @param expirationTime
* @return
*/
boolean isTokenExpired(Date expirationTime);
String getTokenFromRequest(HttpServletRequest request);
}
- 实现类TokenService
package com.dechnic.admin.service.jwt.impl;/**
* @className: TokenService
* @author: houqd
* @date: 2022/7/15
**/
import com.dechnic.admin.service.jwt.ITokenService;
import com.dechnic.pay.configprops.WeChatTokenProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.Map;
/**
* @description:
* @author:houqd
* @time: 2022/7/15 16:28
*/
@Service
public class TokenService implements ITokenService {
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final String TOKEN_PREFIX="Bearer ";
@Autowired
private WeChatTokenProperties weChatTokenProperties;
@Override
public String createToken(String identityId, Map<String, Object> claims) {
Date nowDate = new Date();
// 过期时间
Date expireDate = new Date(nowDate.getTime() + weChatTokenProperties.getExpireTime()* MILLIS_MINUTE);
claims.put("expireDate",expireDate);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(identityId)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512,weChatTokenProperties.getSecret())
.compact();
}
@Override
public Claims getTokenClaim(String token) {
return Jwts.parser()
.setSigningKey(weChatTokenProperties.getSecret())
.parseClaimsJws(token)
.getBody();
}
@Override
public boolean isTokenExpired(Date expirationTime) {
return expirationTime.before(new Date());
}
@Override
public String getTokenFromRequest(HttpServletRequest request) {
String token = request.getHeader(weChatTokenProperties.getHeader());
if (StringUtils.isNotEmpty(token)&& token.startsWith(TOKEN_PREFIX)){
token = token.replace(TOKEN_PREFIX,"");
}
return token;
}
}
2.拦截器配置,拦截请求获取token
- 配置登录拦截器LoginInterCeptor 实现HandlerInterceptor接口
package com.dechnic.admin.interceptor;
import cn.hutool.core.util.ObjectUtil;
import com.dechnic.common.anno.PassToken;
import com.dechnic.common.anno.UserLoginToken;
import com.dechnic.admin.model.bean.user.UserInfo;
import com.dechnic.admin.service.jwt.ITokenService;
import com.dechnic.admin.service.user.UserSearchService;
import com.dechnic.common.exception.BusinessException;
import com.dechnic.common.exception.BusinessExceptionEnum;
import com.dechnic.common.exception.NotLoginException;
import com.dechnic.pay.configprops.WeChatTokenProperties;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.security.auth.login.LoginException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Date;
/**
* @description:
* @author:houqd
* @time: 2022/7/15 14:41
*/
@Slf4j
@Component
public class LoginInterCeptor implements HandlerInterceptor {
@Resource
private ITokenService tokenService;
@Autowired
private WeChatTokenProperties weChatTokenProperties;
@Autowired
private UserSearchService userSearchService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//Token 验证
String token = tokenService.getTokenFromRequest(request);
log.info("==========获取到token:"+token);
// 地址过滤
String uri = request.getRequestURI();
if (uri.contains("/user/oauth/wx/openid/info")){
return true;
}
// 如果不是映射到方法直接通过
if(!(handler instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
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 (StringUtils.isEmpty(token)){
log.error("预付费小程序登陆:"+weChatTokenProperties.getHeader()+"不能为空");
throw new NotLoginException();
}
Claims claims = tokenService.getTokenClaim(token);
Long expireDate = (Long) claims.get("expireDate");
if (ObjectUtil.isEmpty(claims) || tokenService.isTokenExpired(new Date(expireDate))){
log.error("预付费小程序"+weChatTokenProperties.getHeader()+"失效,请重新登陆");
throw new NotLoginException();
}
String username = (String) claims.get("username");
String sysCustomer = (String) claims.get("sysCustomer");
UserInfo userInfo = userSearchService.get(sysCustomer, username);
if (ObjectUtil.isEmpty(userInfo)){
log.error("预付费小程序,商户["+sysCustomer+"],登陆用户名["+username+"]数据库不存在");
throw new NotLoginException();
}
}
}
return true;
}
}
- 配置拦截器 InterceptorConfig 实现 WebMvcConfigurer
用于配置拦截请求 - InterceptorConfig 配置
package com.dechnic.admin.config;
import com.dechnic.admin.interceptor.LoginInterCeptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @description:
* @author:houqd
* @time: 2022/7/15 14:36
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private LoginInterCeptor loginInterCeptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterCeptor).addPathPatterns("/**");
}
}
@PassToken 注解配置
在不需要token 校验的方法上配置此注解,比如登录方法。
package com.dechnic.common.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 用来跳过验证的token
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
@UserLoginToken 注解配置
在需要token 校验的方法上添加此注解
package com.dechnic.common.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 用户登陆后才能操作
* @className: UserLoginToken
* @author: houqd
* @date: 2022/7/15
**/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
3.全局异常及自定义异常配置
- GlobalException 配置
package com.dechnic.common.exception;/**
* @className: GlobalException
* @author: houqd
* @date: 2022/7/16
**/
import com.dechnic.common.dto.AjaxResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
*@description: 全局异常处理
*@author:houqd
*@time: 2022/7/16 8:59
*
*/
@Slf4j
@RestControllerAdvice
@Order(9999)
public class GlobalException {
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(Exception.class)
public AjaxResult handlerException(Exception e){
log.error(e.getMessage());
return AjaxResult.error("服务器出错!");
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(BusinessException.class)
public AjaxResult handlerException(BusinessException e){
log.error(e.getMessage());
return AjaxResult.error(e.getCode(),e.getMessage());
}
}
注意: 如果全局异常没有走:1.检查SpringBoot 能否扫描到
2.添加@Order(9999) 启动顺序
- 自定义异常BusinessException 实现 RuntimeException
package com.dechnic.common.exception;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import javax.persistence.criteria.CriteriaBuilder;
/**
* @description:
* @author:houqd
* @time: 2022/7/6 16:57
*/
@Data
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* 异常对应的返回码
*/
private Integer code;
/**
* 异常对应的描述信息
*/
private final String message;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
}
- 异常枚举类BusinessExceptionEnum
package com.dechnic.common.exception;
/**
* @className: BusinessExceptionEnum
* @author: houqd
* @date: 2022/7/16
**/
public enum BusinessExceptionEnum {
NOT_LOGIN(400,"未登录,或登陆超期"),
PAY_EXCEPTION(600,"微信支付异常"),
SEARCH_ORDER_EXCEPTION(601,"查询订单异常");
private Integer code;
private String error;
BusinessExceptionEnum(Integer code, String error) {
this.code = code;
this.error = error;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
- 自定义异常子类NotLoginException
package com.dechnic.common.exception;/**
* @className: NotLoginException
* @author: houqd
* @date: 2022/7/16
**/
/**
*@description:
*@author:houqd
*@time: 2022/7/16 10:20
*
*/
public class NotLoginException extends BusinessException{
public NotLoginException() {
super(BusinessExceptionEnum.NOT_LOGIN.getCode(), BusinessExceptionEnum.NOT_LOGIN.getError());
}
}
yaml 配置
#微信token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: abcdefghijklmnopqrstuvwxyz
# 令牌有效期(单位分钟)一天 1400
expireTime: 1400
TestController
package com.dechnic.admin.web;/**
* @className: TestController
* @author: houqd
* @date: 2022/7/16
**/
import com.dechnic.common.dto.AjaxResult;
import com.dechnic.common.exception.BusinessException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*@description:
*@author:houqd
*@time: 2022/7/16 9:11
*
*/
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/test1")
public AjaxResult test1(){
int a = 1/0;
return AjaxResult.ok();
}
@GetMapping("/test2")
public AjaxResult test2(){
String name = null;
if (name == null){
throw new BusinessException(400,"名称不能为空");
}
return AjaxResult.ok();
}
}
4.微信小程序端处理异常
// 全路径的请求
function allUrlReq(url, data, fun) {
var token = wx.getStorageSync('token');
wx.request({
url: url,
data: data,
method: 'post',
header: { 'Content-Type': 'application/x-www-form-urlencoded','Authorization':'Bearer '+token},
success: function (res) {
// console.log("========请求返回:"+JSON.stringify(res))
const status = res.data.status;
if(status == 400){// 未登录
const err = res.data.error;
console.log(err)
console.log("=====未登录或登录超期,跳转到首页====")
wx.navigateTo({
url: '../login/login'
})
}else if(status == 600|| status == 601){
// 600 支付异常,601查询订单异常
const err = res.data.error;
util.showToast(err);
}
return typeof fun == "function" && fun(res.data);
},
fail: function (err) {
console.log("======error:"+err.errMsg)
return typeof fun == "function" && fun(false);
}
})
}
const requestPromise = (url, data) => {
// 返回一个Promise实例对象
return new Promise((resolve, reject) => {
wx.request({
url: rootUrl + url,
data: data,
method: 'post',
header: { 'Content-Type': 'application/x-www-form-urlencoded' },
success: function (res) {
if (res.statusCode != 200) {
reject(res.data);
return;
}
if (res.data.status == 0 && res.data.type == 'notLogin') {
var token = wx.getStorageSync("token");
wx.navigateBack({
delta: 1,
success() {
wx.navigateTo({
url: '../login/login',
})
}
})
wx.removeStorageSync("token")
wx.removeStorageSync("userInfo")
}
resolve(res.data);
},
fail: function (res) {
reject(res.data);
},
})
})
}
5. Nginx 配置 支持ssl 登录
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 30090 ssl;
server_name localhost;
ssl_certificate D:/nginx-1.20.1/cert/xxx.pem;
ssl_certificate_key D:/nginx-1.20.1/cert/xxx.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4:!DHE;
ssl_prefer_server_ciphers on;
#proxy_set_header X-Forwarded-Host $host;
#proxy_set_header X-Forwarded-Server $host;
#proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#proxy_set_header Host $host:$server_port; #这里是重点,这样配置才不会丢失端口
location /openapi {
proxy_pass http://localhost:8082;
proxy_set_header Host $host;
proxy_set_header X-Real_IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
}
location /api{
proxy_pass http://localhost:8081;
proxy_set_header Host $host;
proxy_set_header X-Real_IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}
4.小结:
- 引用jjwt 配置 token 方法。
- 创建拦截器拦截用户请求,解析token 是否合法
- 创建全局异常,抛出自定义异常给客户端。
- 客户端接收到异常返回解析执行回到登录页操作。