SpringBoot2+ JWT 前后端分离 自定义token 校验,及自定义全局异常处理

1.知识准备

1.默认已熟悉SpringBoot2 .
2.Maven 相关知识。
3.JWT相关知识

2. 项目背景:

前后端分离项目,后端采用SpringBoo2.3.12.RELEASE ,
前端微信小程序,实现小程序登录后,后端通过jwt 生成token 返回微信小程序端存储到小程序本地存储。下次再请求携带token, 后台校验token 是否存在,及token 是否过期,后台根据token 解析出携带的用户系统,再从缓存或数据库查询用户信息是否存在,如果不存在或超期,则抛出自定义异常,返回给小程序端,小程序端根据异常状态码判断异常类型,如果是登录异常则跳到登录页重新登录。
下次请求携带

3.开发步骤

1.JWT 配置

  1. 引入 jjwt maven 依赖
 		<!--jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
  1. 创建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.全局异常及自定义异常配置

  1. 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) 启动顺序

  1. 自定义异常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;
    }

}

  1. 异常枚举类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;
    }
}
  1. 自定义异常子类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.小结:

  1. 引用jjwt 配置 token 方法。
  2. 创建拦截器拦截用户请求,解析token 是否合法
  3. 创建全局异常,抛出自定义异常给客户端。
  4. 客户端接收到异常返回解析执行回到登录页操作。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java EE常用框架 WebService 介绍 基于Web的服务。它使用Web(HTTP)方式,接收和响应外部系统的某种请求。从而实现远程调用 术语 XML. Extensible Markup Language -扩展性标记语言 WSDL – WebService Description Language – Web服务描述语言。 SOAP-Simple Object Access Protocol(简单对象访问协议) SOA(Service-Oriented Architecture) :面向服务的架构 它是一种思想,IBM大力倡导是即插即用的,IBM大力提倡,希望以组装电脑的方式来开发应用 它是目录服务,通过该服务可以注册和发布webservcie,以便第三方的调用者统一调用 使用: 二、我们可以使用Java自带的WsImport来实现本地代理。这种方法会将WebService翻译成Java类,我们使用类一样去访问WebService就行了。非常好用。 三、除了调用别人发布的webService,也可以自己发布WebService服务 四、CXF框架可以与spring无缝连接,就不用我们自己Endpoint了。它还能记录日志之类的 五、我们还可以使用Idea下的webservice,能够使用图形画面的方式获取本地代理和生成WSDL文件。 Activiti 介绍 Activiti5是一个业务流程管理(BPM)框架 如果我们的业务是比较复杂的话,我们才会用到工作流! 使用Activiti的步骤 定义工作流 画一个BPMN图就可以了 部署工作流 执行工作流-->按照我们定义的工作流来执行 工作流在执行的过程中肯定会涉及到很多数据,Activiti是默认提供数据库表给我们使用的 Activiti工作流框架快速入门: 定义工作流,使用插件来把我们的流程图画出来。这个流程图就是我们定义的工作流。 工作流引擎是工作流的核心,能够让我们定义出来的工作流部署起来。 由于我们使用工作流的时候是有很多数据产生的,因此Activiti是将数据保存到数据库表中的。这些数据库表由Actitviti创建,由Activiti维护。 部署完的工作流是需要手动去执行该工作流的。 根据由谁处理当前任务,我们就可以查询出具体的任务信息。 根据任务的id,我们就可以执行任务了。 细节 流程定义:涉及到了四张数据库表 我们可以通过API把我们的流程定义图读取出来 可以根据查询最新版本的流程定义 删除流程定义 部署流程定义的时候也可以是ZIP文件 流程运行:涉及到两个对象,四张数据库表: 流程实例 获取流程实例和任务的历史信息 判断流程实例是否为空来判断流程是否结束了 查看正在运行服务的详细信息 通过流程实例来开启流程 流程变量:它涉及到了两张表。 流 程变量实际上就是我们的条件。 作用 我们可以在流程开始的时候设置流程变量,在任务完成的时候设置流程变量。 运行时服务和流程任务都可以设置流程变量。 连线 通过连线我们可以在其中设置条件,根据不同的条件流程走不同的分支 排他网关 SpringData JPA 简介 API Repository接口 PagingAndSortingRepository JpaRepository JpaSpecificationExecutor 过滤条件查询接口 注解 nameQuery注解 SQL命名,调用的时候根据名称调用 查询注解 1,targetEntity 属性表示默认关联的实体类型,默认为当前标注的实体类。 2,cascade属性表示与此实体一对一关联的实体的级联样式类型。 3,fetch属性是该实体的加载方式,默认为即时加载EAGER 4,optional属性表示关联的该实体是否能够存在null值,默认为ture,如果设置为false,则该实体不能为null, 5, mapperBy属性:指关系被维护端 1,@JoinColumn注释是保存表与表之间关系的字段 2,如果不设置name,默认name = 关联表的名称+”-“+关联表

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值