从零构建后端项目-配置Shiro+JWT

news2024/11/26 8:35:10

目录

Shiro和JWT技术

一、Shiro简介

什么是认证? 

什么是授权? 

Shiro靠什么做认证与授权的? 

二、JWT简介

JWT可以用在单点登录的系统中 

JWT兼容更多的客户端 

创建JwtUtil工具类

一、导入依赖库

二、定义密钥和过期时间

三、创建JWT工具类

把令牌封装成认证对象

创建OAuth2Realm类

刷新令牌应该如何设计

一、为什么要刷新Token的过期时间?

二、客户端如何更新令牌?

三、如何在响应中添加令牌?

创建存储令牌的媒介类

创建OAuth2Filter类

创建ShiroConfig类

利用切面类向客户端返回新令牌

精简返回给客户端的异常内容


Shiro和JWT技术

一、Shiro简介

        Shiro【apache】是Java领域非常知名的认证( Authentication )与授权( Authorization )框架,用以替代JavaEE中的JAAS(认证与授权服务)功能。相较于其他认证与授权框架,Shiro设计的非常简单,所以广受好评。任意JavaWeb项目都可以使用Shiro框架,而Spring Security必须要使用在Spring项目中。所以Shiro的适用性更加广泛。像什么 JFinal 和 Nutz 非Spring框架都可以使用Shiro,而不能使用Spring Security框架。 

什么是认证? 

        认证就是要核验用户的身份,比如说通过用户名和密码来检验用户的身份。说简单一些,认证就是登陆。登陆之后Shiro要记录用户成功登陆的凭证。 

什么是授权? 

        授权是比认证更加精细度的划分用户的行为。比如说一个教务管理系统中,学生登陆之后只能查看信息,不能修改信息。而班主任就可以修改学生的信息。这就是利用授权来限定不同身份用户的行为。 

Shiro靠什么做认证与授权的? 

        Shiro可以利用 HttpSession 或者 Redis 存储用户的登陆凭证,以及角色或者身份信息。然后利用过滤器(Filter),对每个Http请求过滤,检查请求对应的 HttpSession 或者 Redis 中的认证与授权信息。如果用户没有登陆,或者权限不够,那么Shiro会向客户端返回错误信息。也就是说,我们写用户登陆模块的时候,用户登陆成功之后,要调用Shiro保存登陆凭证。然后查询用户的角色和权限,让Shiro存储起来。将来不管哪个方法需要登陆访问,或者拥有特定的角色跟权限才能访问,我们在方法前设置注解即可,非常简单。

二、JWT简介

        JWT(Json Web Token), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。JWT一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。 

JWT可以用在单点登录的系统中 

        传统的 JavaWeb 项目,利用 HttpSession 保存用户的登陆凭证。如果后端系统采用了负载均衡设计,当用户在A节点成功登陆,那么登陆凭证保存在A节点的 HttpSession 中。如果用户下一个请求被负载均衡到了B节点,因为B节点上面没有用户的登陆凭证,所以需要用户重新登录,这个体验太糟糕了。

 

        如果用户的登陆凭证经过加密( Token )保存在客户端,客户端每次提交请求的时候,把Token 上传给后端服务器节点。即便后端项目使用了负载均衡,每个后端节点接收到客户端上传的Token之后,经过检测,是有效的 Token ,于是就断定用户已经成功登陆,接下来就可以提供后端服务了。

 

JWT兼容更多的客户端 

        传统的 HttpSession 依靠浏览器的 Cookie 存放 SessionId ,所以要求客户端必须是浏览器。现在的JavaWeb系统,客户端可以是浏览器、APP、小程序,以及物联网设备。为了让这些设备都能访问到JavaWeb项目,就必须要引入JWT技术。JWT的 Token 是纯字符串,至于客户端怎么保存,没有具体要求。只要客户端发起请求的时候,附带上 Token 即可。所以像物联网设备,我们可以用 SQLite 存储 Token 数据。

创建JwtUtil工具类

        JWT的 Token 要经过加密才能返回给客户端,包括客户端上传的 Token ,后端项目需要验证核实。于是我们需要一个JWT工具类,用来 加密Token 和 验证Token 的有效性。

一、导入依赖库

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.5.3</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.5.3</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.11</version>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpcore</artifactId>
    <version>4.4.13</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

二、定义密钥和过期时间

        我建议大家把密钥和过期时间定义到SpringBoot配置文件中,然后再值注入到JavaBean中,这样维护起来比较方便。

emos: 
    jwt: 
        #密钥
        secret: abc123456
        #令牌过期时间(天)
        expire: 5 
        #令牌缓存时间(天数)
        cache-expire: 10

三、创建JWT工具类

在 com.example.emos.wx.config.shiro 中创建 JwtUtil 类。

package com.example.emos.wx.config.shiro;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.emos.wx.exception.EmosException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;

@Component  // 工具类需要被Spring使用,加@Component注解。
@Slf4j
public class JwtUtil {
    //密钥
    @Value("${emos.jwt.secret}")
    private String secret;

    //过期时间(天)
    @Value("${emos.jwt.expire}")
    private int expire;

    public String createToken(int userId) {
        Date date = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, expire).toJdkDate();
        Algorithm algorithm = Algorithm.HMAC256(secret); //创建加密算法对象
        JWTCreator.Builder builder = JWT.create();
        String token = builder.withClaim("userId",
userId).withExpiresAt(date).sign(algorithm);
        return token;
    }

    public int getUserId(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("userId").asInt();
        } catch (Exception e) {
            throw new EmosException("令牌无效");
        }
    }

    public void verifierToken(String token) {
        Algorithm algorithm = Algorithm.HMAC256(secret); //创建加密算法对象
        JWTVerifier verifier = JWT.require(algorithm).build();
        verifier.verify(token);
    }
}

把令牌封装成认证对象

        Shiro框架的认证需要用到认证对象。

        ShiroConfig配置类被SpringBoot项目读取之后,SpringBoot项目就可以使用Shiro框架和JWT技术了。


        上一小节,我们通过JwtUtil类可以生成 Token ,这个 Token 我们是要返回给客户端的。接下来我们要把 JWT 和 Shiro框架 对接起来,这样 Shiro框架 就会拦截所有的Http请求,然后验证请求提交的 Token 是否有效。 

 

        客户端提交的Token不能直接交给Shiro框架,需要先封装成 AuthenticationToken 类型的对象,所以我们我们需要先创建 AuthenticationToken 的实现类。 

在 com.example.emos.wx.config.shiro 中创建 OAuth2Token 类。 

package com.example.emos.wx.config.shiro;
import org.apache.shiro.authc.AuthenticationToken;

public class OAuth2Token implements AuthenticationToken {
    private String token;
 
    public OAuth2Token(String token){
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

创建OAuth2Realm类

        OAuth2Realm 类是 AuthorizingRealm 的实现类,我们要在这个实现类中定义认证和授权的方法。因为认证与授权模块设计到用户模块和权限模块,现在我们还没有真正的开发业务模块,所以我们这里先暂时定义空的认证与授权方法,把Shiro和JWT整合起来,在后续章节我们再实现认证与授权。

在 com.example.emos.wx.config.shiro 中创建 OAuth2Realm 类。 

package com.example.emos.wx.config.shiro;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Set;

@Component
public class OAuth2Realm extends AuthorizingRealm {
    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof OAuth2Token;
    }

    /**
     * 授权(验证权限时调用)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //TODO 查询用户的权限列表
        //TODO 把权限列表添加到info对象中
        return info;
    }

    /**
     * 认证(登录时调用)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //TODO 从令牌中获取userId,然后检测该账户是否被冻结。
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
        //TODO 往info对象中添加用户信息、Token字符串
        return info;
    }
}

刷新令牌应该如何设计

一、为什么要刷新Token的过期时间?

        我们在定义JwtUtil工具类的时候,生成的 Token 都有过期时间。那么问题来了,假设 Token 过期时间为15天,用户在第14天的时候,还可以免登录正常访问系统。但是到了第15天,用户的Token过期,于是用户需要重新登录系统。 

        HttpSession 的过期时间比较优雅,默认为15分钟。如果用户连续使用系统,只要间隔时间不超过15分钟,系统就不会销毁 HttpSession 对象。JWT的令牌过期时间能不能做成 HttpSession 那样超时时间,只要用户间隔操作时间不超过15天,系统就不需要用户重新登录系统。实现这种效果的方案有两种: 双Token 和 Token缓存 ,这里重点讲一下 Token 缓存方案。 

 

 

        Token缓存方案是把 Token 缓存到Redis,然后设置Redis里面缓存的 Token 过期时间为正常Token 的1倍,然后根据情况刷新 Token 的过期时间。 

Token失效,缓存也不存在的情况 

        当第15天,用户的 Token 失效以后,我们让Shiro程序到Redis查看是否存在缓存的 Token ,如果这个 Token 不存在于Redis里面,就说明用户的操作间隔了15天,需要重新登录。 

Token失效,但是缓存还存在的情况 

        如果Redis中存在缓存的 Token ,说明当前 Token 失效后,间隔时间还没有超过15天,不应该让用户重新登录。所以要生成新的 Token 返回给客户端,并且把这个 Token 缓存到Redis里面,这种操作成为刷新 Token 过期时间。

二、客户端如何更新令牌?

        在我们的方案中,服务端刷新 Token 过期时间,其实就是生成一个新的 Token 给客户端。那么客户端怎么知道这次响应带回来的Token是更新过的呢?这个问题很容易解决。 

 

        只要用户成功登陆系统,当后端服务器更新 Token 的时候,就在响应中添加 Token 。客户端那边判断每次Ajax响应里面是否包含 Token ,如果包含,就把 Token 保存起来就可以了。

三、如何在响应中添加令牌?

        我们定义 OAuth2Filter 类拦截所有的HTTP请求,一方面它会把请求中的 Token 字符串提取出来,封装成对象交给Shiro框架;另一方面,它会检查 Token 的有效性。如果 Token 过期,那么会生成新的 Token ,分别存储在 ThreadLocalToken 和 Redis 中。 

        之所以要把 新令牌 保存到 ThreadLocalToken 里面,是因为要向 AOP切面类 传递这个 新令牌 。虽然 OAuth2Filter 中有 doFilterInternal() 方法,我们可以得到响应并且写入 新令牌 。但是这个做非常麻烦,首先我们要通过IO流读取响应中的数据,然后还要把数据解析成JSON对象,最后再放入这个新令牌。如果我们定义了 AOP切面类 ,拦截所有Web方法返回的 R对象 ,然后在 R对象 里面添加 新令牌 ,这多简单啊。但是 OAuth2Filter 和 AOP 切面类之间没有调用关系,所以我们很难把 新令牌 传给 AOP切面类 。 

        这里我想到了 ThreadLocal ,只要是同一个线程,往 ThreadLocal 里面写入数据和读取数据是完全相同的。在Web项目中,从 OAuth2Filter 到 AOP切面类 ,都是由同一个线程来执行的,中途不会更换线程。所以我们可以放心的把新令牌保存都在 ThreadLocal 里面, AOP切面类 可以成功的取出新令牌,然后往 R对象 里面添加新令牌即可。ThreadLocalToken 是我自定义的类,里面包含了 ThreadLocal 类型的变量,可以用来保存线程安全的数据,而且避免了使用线程锁。

创建存储令牌的媒介类

在 com.example.emos.wx.config.shiro 中创建 ThreadLocalToken 类。 

package com.example.emos.wx.config.shiro;
import org.springframework.stereotype.Component;

@Component
public class ThreadLocalToken {
    private ThreadLocal local=new ThreadLocal();

    public void setToken(String token){
        local.set(token);
    }

    public String getToken(){
        return (String) local.get();
    }

    public void clear(){
        local.remove();
    }
}

创建OAuth2Filter类

注意事项:因为在 OAuth2Filter 类中要读写 ThreadLocal 中的数据,所以 OAuth2Filter 类必须要设置成多例的@Scope("prototype") ,否则 ThreadLocal 将无法使用。

在配置文件中,添加JWT需要用到的密钥、过期时间和缓存过期时间。 

emos:
    jwt:
        #密钥
        secret: abc123456
        #令牌过期时间(天)
        expire: 5
        #令牌缓存时间(天数)
        cache-expire: 10

 在 com.example.emos.wx.config.shiro 中创建 OAuth2Filter 类。

package com.example.emos.wx.config.shiro;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

@Component
@Scope("prototype")
public class OAuth2Filter extends AuthenticatingFilter {
    @Autowired
    private ThreadLocalToken threadLocalToken;

    @Value("${emos.jwt.cache-expire}")
    private int cacheExpire;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 拦截请求之后,用于把令牌字符串封装成令牌对象
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            return null;
        }
        return new OAuth2Token(token);
    }

    /**
     * 拦截请求,判断请求是否需要被Shiro处理
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest req = (HttpServletRequest) request;
        // Ajax提交application/json数据的时候,会先发出Options请求
        // 这里要放行Options请求,不需要Shiro处理
        if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
        // 除了Options请求之外,所有请求都要被Shiro处理
        return false;
    }

    /**
     * 该方法用于处理所有应该被Shiro处理的请求
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        resp.setHeader("Content-Type", "text/html;charset=UTF-8");
        //允许跨域请求
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        threadLocalToken.clear();
        //获取请求token,如果token不存在,直接返回401
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            return false;
        }
        try {
            jwtUtil.verifierToken(token); //检查令牌是否过期
        } catch (TokenExpiredException e) {
            //客户端令牌过期,查询Redis中是否存在令牌,如果存在令牌就重新生成一个令牌给客户端
            if (redisTemplate.hasKey(token)) {
                redisTemplate.delete(token);//删除令牌
                int userId = jwtUtil.getUserId(token);
                token = jwtUtil.createToken(userId); //生成新的令牌
                //把新的令牌保存到Redis中
                redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS);
                //把新令牌绑定到线程
                threadLocalToken.setToken(token);
            } else {
                //如果Redis不存在令牌,让用户重新登录
                resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
                resp.getWriter().print("令牌已经过期");
                return false;
            }
        } catch (JWTDecodeException e) {
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            return false;
        }
        boolean bool = executeLogin(request, response);
        return bool;
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
        resp.setContentType("application/json;charset=utf-8");
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        try {
            resp.getWriter().print(e.getMessage());
        } catch (IOException exception) {
        }
        return false;
    }

    /**
     * 获取请求头里面的token
     */
    private String getRequestToken(HttpServletRequest httpRequest) {
        //从header中获取token
        String token = httpRequest.getHeader("token");
        //如果header中不存在token,则从参数中获取token
        if (StringUtils.isBlank(token)) {
            token = httpRequest.getParameter("token");
        }
        return token;
    }

    @Override
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        super.doFilterInternal(request, response, chain);
    }
}

创建ShiroConfig类

        上一节创建的Filter类不是标准意义上的Java web里面的过滤器Java类。因为我们给自定义的 filter类 加的是@component注解,而不是传统意义上的 @webfilter 注解。所以我们创建的 过滤器Java类,只是一个普通的Java bean而已。如果我们想让这个Java bean可以拦截 http 请求,那么就需要把它配置到shiro框架。包括上一节课代码,在filter类里面,验证了token,如果没有问题,于是就去调用 executeLogin 方法。执行这个方法会让shiro框架,调用realm类,所以这节咱们把realm类也配置到shiro框架上面。

        xml 中 <bean></bean> 演变为 @configuration @bean。被Spring框架自动执行,执行后 方法返回对象 自动注册给Spring框架。自动执行时可以用@component修饰的类 作 参数。


        我们要创建的 ShiroConfig 类,是用来把 OAuth2Filter 和 OAuth2Realm 配置到Shiro框架,这样我们辛苦搭建的Shiro+JWT才算生效。 

在com.example.emos.wx.config.shiro中创建ShiroConfig类。

package com.example.emos.wx.config.shiro;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    @Bean("securityManager")
    public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(oAuth2Realm);
        securityManager.setRememberMeManager(null);
        return securityManager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, OAuth2Filter oAuth2Filter) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        //oauth过滤
        Map<String, Filter> filters = new HashMap<>();
        filters.put("oauth2", oAuth2Filter);
        shiroFilter.setFilters(filters);
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/app/**", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/swagger-resources/**", "anon");
        filterMap.put("/captcha.jpg", "anon");
        filterMap.put("/user/register", "anon");
        filterMap.put("/user/login", "anon");
        filterMap.put("/test/**", "anon");
        filterMap.put("/**", "oauth2");
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }

    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

利用切面类向客户端返回新令牌

@Aspect 切面类

controller.*.*  包 类 方法


        我们在写 OAuth2Filter 的时候,把更新后的令牌写到 ThreadLocalToken 里面的 ThreadLocal 。那么这个小节,我们要创建 AOP切面类 ,拦截所有Web方法的返回值,在返回的 R对象 中添加更新后的令牌。 

在 com.example.emos.wx.aop 中创建 TokenAspect 类。

package com.example.emos.wx.aop;
import com.example.emos.wx.common.util.R;
import com.example.emos.wx.config.shiro.ThreadLocalToken;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TokenAspect {
    @Autowired
    private ThreadLocalToken threadLocalToken;
    @Pointcut("execution(public * com.example.emos.wx.controller.*.*(..))")
    public void aspect() {
    }

    @Around("aspect()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        R r = (R) point.proceed(); //方法执行结果
        String token = threadLocalToken.getToken();
        //如果ThreadLocal中存在Token,说明是更新的Token
        if (token != null) {
            r.put("token", token); //往响应中放置Token
            threadLocalToken.clear();
        }
        return r;
    }
}

精简返回给客户端的异常内容

        filter把未登录的请求的拦截下来了,把错误消息直接写在响应里了,没有让请求传递 执行web方法,web方法未执行根本不会触发异常。用户没有登录就直接发请求,那么咱们后端系统不会出现异常,就没有必要在这里判断未登录异常。

@RestControllerAdvice:rest风格的@ControllerAdvice

@ExceptionHandler 必须要有的


        之前我们测试sayHello()方法的时候,因为客户端提交的参数不正确,所以后端系统向客户端返回了大量的异常内容。这里我们要对返回的异常内容做一下精简。 

在 com.example.emos.wx.config 中,创建 ExceptionAdvice 类。 

package com.example.emos.wx.config;
import com.example.emos.wx.exception.EmosException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    public String validExceptionHandler(Exception e) {
        log.error("执行异常",e);
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
            //将错误信息返回给前台
            return exception.getBindingResult().getFieldError().getDefaultMessage();
        }
        else if(e instanceof EmosException){ EmosException exception = (EmosException) e;
            return exception.getMsg();
        }
        else if(e instanceof UnauthorizedException){
            return "你不具有相关权限";
        }
        else {
            return "后端执行异常";
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/666594.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

强大的Kotlin也能搞定,测试利器MockK你不能不知道

目录 前言&#xff1a; 为什么需要MockK 关键字 Mock Kotlin的类时报错 静态方法如何Mock Jmockit MockK使用示例 普通使用 mockkObject mockkStatic mock private method Context Mock 遇到的一些小坑 最后 前言&#xff1a; MockK是一个强大且易于使用的Kotli…

爆肝整理,手机App接口测试大全指南,看这篇就够了...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 App和Web请求服务…

用prim和kruskal算法求最小生成树问题

最短网络 题目http://ybt.ssoier.cn:8088/problem_show.php?pid1350 #include<bits/stdc.h> using namespace std; const int N110; int w[N][N]; bool st[N]; int dist[N]; int n,res0; void prim() {memset(dist,0x3f,sizeof dist);dist[1]0;//初始化第一个点到自己…

优化回声消除过程:舒适噪声生成算法的应用与原理

在回声消除中&#xff0c;舒适噪声生成(Comfort Noise Generator&#xff0c;CNG)算法是一种常用的技术&#xff0c;它被用来减轻回声消除过程中产生的不适感和声音失真。舒适噪声生成算法通过添加特定的噪声信号来模拟人耳的听觉特性&#xff0c;以改善声音的自然度和舒适度。…

Cpp面试题:main函数执行以前,还会执行什么代码?

Cpp面试题:main函数执行以前&#xff0c;还会执行什么代码&#xff1f; 在 main() 函数执行之前和之后&#xff0c;C 程序可能会执行以下代码&#xff1a; 静态对象的构造函数&#xff1a;如果程序中有静态对象&#xff08;全局变量或静态成员变量&#xff09;&#xff0c;它们…

深入了解 vcruntime140 文件,从多方面解析vcruntime140

vcruntime140 是一个重要的文件&#xff0c;它在 Windows 系统中扮演着重要的角色。如果不小心缺失了&#xff0c;那么你的电脑就会出现问题&#xff0c;今天我们就来探讨一下vcruntime140这个文件&#xff0c;分别从背景和预防丢失&#xff0c;到如何修复丢失vcruntime140来给…

Elasticsearch(十)搜索---搜索匹配功能①--查询所有文档和term级别查询

一、前言 之前的学习我们已经了解了搜索的辅助功能&#xff0c;从这一章开始就是ES真正核心的功能&#xff0c;搜索。针对不同的数据类型&#xff0c;ES提供了很多搜索匹配功能&#xff1a;既有进行完全匹配的term搜索&#xff0c;也有按照范围匹配的range搜索&#xff1b;既有…

一文让你学会接口自动化测试框架!

目录 前言&#xff1a; 自动化测试 接口自动化测试的价值 接口自动化测试如何开展 接口自动化测试框架 前言&#xff1a; 接口自动化测试是指利用程序自动化地执行API接口测试&#xff0c;可以提高测试效率和准确性。 自动化测试 自动化测试&#xff0c;这几年行业内的…

一个悄然崛起的AI开源项目!

众所周知&#xff0c;最近这半年AI相关的话题实在是火到出圈。尤其是生成式AI的流行&#xff0c;让我们普通人也可以近距离地接触和应用AI。这其中最典型的就是ChatGPT。 那除了ChatGPT&#xff0c;还有一个非常实用的领域&#xff0c;也是我们今天要讨论的话题&#xff0c;那…

​低代码让传统软件开发土掉渣了

正所谓“让机器去做无聊的事情&#xff0c;让人类去创造美好的事物”。 在当今数码化时代&#xff0c;企业如何更快捷、高效的开发应用是众所周知的难题。传统开发方式需要多名开发人员耗费大量时间精力开发&#xff0c;期间还需要经历漫长的测试和上线过程。 要在这个竞争激烈…

AI绘图软件分享:Midjourney 基础教程(二)

大家好&#xff0c;我是权知星球&#xff0c;今天继续给大家介绍AI绘图软件分享&#xff1a;Midjourney 基础教程&#xff08;二&#xff09; ⼀、Midjourney 服务器介绍 1.Discord 软件介绍 Midjourney AI 绘画服务基于 Discord 软件的&#xff0c;它的绘画功能&#xff0c;…

【AUTOSAR】UDS协议的代码分析与解读(十一)----UDS例程控制31h请求下载 34h

8.15 例程控制 RoutineControl (31h) 此服务用于启动程序 、停止程序和请求程 序执行结果。例程由 两字节的例程标识符 (RoutineIdentifier)来确定。 8.15.1 报文格式 表 71 例程控制服务的请求报文 Byte Name Cvt Value (Hex) #1 RequestServiceIdentifier M 31 #2 …

部署运行jar包方法全解docker镜像打包部署等

基本方法 java -jar 对应的jar包名字 永久后台方法 有一种叫做“nohup”的命令&#xff0c;该命令可以让您的应用程序在后台运行&#xff0c;即使您已经断开了与终端的连接也能保持运行状态。 nohup 命令的语法为&#xff1a; nohup command arg1 arg2 ... argN &其中…

金三银四互联网大厂秋招精选 1160 道 Java 面试题答案整理(2023 最新版)

今年的大环境而言&#xff0c;面试成功的难度比往年高了很多&#xff0c;很明显的感受就是&#xff1a;对于今年的 java 开发朋友面试&#xff0c;无论一面还是二面&#xff0c;都开始考验一个 Java 程序员的技术功底和基础。Java 基础掌握不牢&#xff0c;对于一个开发人员来说…

北斗高精度定位赋能智慧港口,千寻位置解决方案落地应用

港口是交通运输的重要节点&#xff0c;也是国家经济发展的重要支撑&#xff0c;其作业效率直接影响着运营效益。随着全球数字化技术的不断革新&#xff0c;我国港口逐渐从传统模式向智能化、数字化的“智慧模式”转变。在这一转型过程中&#xff0c;高精度技术应用的作用愈发重…

伊朗上下5000年简史

提起伊朗&#xff0c;你脑海中首先浮现的是什么&#xff1f; 混乱、保守、战争&#xff1f;穆斯林&#xff1f;抑或是石油&#xff0c;核武器&#xff1f; 这些附着在伊朗头上的标签&#xff0c;使很多人忽略了&#xff0c;它是一个拥有着5000年历史的文明古国&#xff1b;在…

保护视力的软件:定时提醒你休息的桌面工具EyeLeo

文章目录 保护视力的软件&#xff1a;定时提醒你休息的桌面工具EyeLeo什么是EyeLeo为什么要使用&#xff1f;它为什么如此重要&#xff1f;EyeLeo特征 使用说明 保护视力的软件&#xff1a;定时提醒你休息的桌面工具EyeLeo 什么是EyeLeo 官网&#xff1a;http://www.eyeleo.c…

django新手教程

Django简介 Django是开源的、大而且全的Web应用框架。 它独具特色&#xff0c;采用了MTV设计模式。 它也是一款用来构建服务器的框架。这一概念如何理解呢&#xff1f; 应用程序有两种模式&#xff1a;C/S、B/S。 C/S是客户端与服务器端&#xff0c;这类程序一般能独立运行…

【Python】高级语法:推导式、迭代器、生成器、装饰器

原文作者&#xff1a;我辈李想 版权声明&#xff1a;文章原创&#xff0c;转载时请务必加上原文超链接、作者信息和本声明。 文章目录 一、推导式1.列表推导式2.集合推导式3.字典推导式 二、迭代器三、生成器1.yield 生成器2.元组生成器3.生成器中重要方法 四、装饰器1.函数装饰…

谈谈电商API!

近年来&#xff0c;随着互联网和移动互联网技术的不断发展&#xff0c;电商行业成为了一种新兴的商业模式。电商平台实现了互联网和商品销售的深度融合&#xff0c;成为经济社会发展的重要组成部分。而电商API&#xff08;Application Programming Interface, 应用程序接口&…