Shiro整合JWT实战

举报
码农参上 发表于 2022/04/06 15:44:28 2022/04/06
【摘要】 JSON Web Token(JWT)是为了在网络应用间传递声明而执行的一种基于JSON的开放标准。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。token可以直接被用于认证,也可被加密。我们在springboot+shiro的基础上,整合jwt模块,对其进行扩展,实现无状态认证加鉴权。JWT实现认证思路:因为要实现无状态,所以jwt需...

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

我们在springboot+shiro的基础上,整合jwt模块,对其进行扩展,实现无状态认证加鉴权。

JWT实现认证思路:

  • 因为要实现无状态,所以jwt需要关闭shiro的session管理
  • 用户第一次登录成功时,使用jwt返回token
  • 在后续的请求中携带token,每次请求都会对token携带的用户信息进行验证,并完成后续认证和鉴权

1、导入Maven依赖:

<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.7.0</version>
</dependency>

2、封装JwtToken来替换shiro的原生token,需要实现AuthenticationToken 接口:

public class JwtToken implements AuthenticationToken {
   private final String token;

   public JwtToken(String token) {
       this.token = token;
   }

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

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

3、添加一个工具类JwtUtil来操作token:

public class JwtUtil {
   public static final String ACCOUNT = "username";
   public static final long EXPIRE_TIME = 30 * 60 * 1000;

   public static boolean verify(String token, String username, String secret) {
       try{
           Algorithm algorithm = Algorithm.HMAC256(secret);
           JWTVerifier verifier = JWT.require(algorithm)
                   .withClaim(ACCOUNT, username)
                   .build();

           DecodedJWT jwt = verifier.verify(token);
           return true;
       }catch (Exception e){
           e.printStackTrace();
           return false;
       }
   }

   public static String getClaimField(String token,String claim){
       try{
           DecodedJWT jwt = JWT.decode(token);
           return jwt.getClaim(claim).asString();
       }catch (JWTDecodeException e){
           e.printStackTrace();
           return  null;
       }
   }
   
   public static String sign(String username, String secret) {
       Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
       Algorithm algorithm = Algorithm.HMAC256(secret);
       return JWT.create()
               .withClaim(ACCOUNT, username)
               .withExpiresAt(date)
               .sign(algorithm);
   }
}

JwtUtil类中主要提供了三个方法:

  • sign方法用于生成附带过期时间的签名,创建过程中可以在claim中存放一些信息,通常可以用来携带用户信息
  • verify方法中,根据密码生成jwt校验器,校验token是否正确,和sign方法使用相同的加密方式
  • getClaimField方法用于获得token中指定字段的信息

4、添加JwtFilter 拦截器,继承AccessControlFilter 类,验证从请求的header中取出的token信息:

public class JwtFilter extends AccessControlFilter {
    public static String ACCESS_TOKEN = "Access-Token";

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        // 解决跨域问题
        if(HttpMethod.OPTIONS.toString().matches(req.getMethod())) {
            return true;
        }
        if (isLoginAttempt(request, response)) {
            JwtToken token = new JwtToken(req.getHeader(ACCESS_TOKEN));
            try {
                getSubject(request, response).login(token);
                return true;
            } catch (Exception e) {
            }
        }
        onLoginFail(response);
        return false;
    }

    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader(ACCESS_TOKEN);
        return authorization != null;
    }

    //登录失败时默认返回401状态码
    private void onLoginFail(ServletResponse response) throws IOException {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.getWriter().write("login fail");
    }
}

这里首先会调用isAccessAllowed方法,均会返回false,之后所有权限认证统一调用onAccessDenied方法进行处理。在onAccessDenied方法中,会从请求的header中拿出token并尝试登录验证。

5、创建JwtRealm 继承AuthorizingRealm 类,Realm中实现了shiro认证的主要功能,主要包括认证鉴权两个方面:

public class JwtRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

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

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = principals.toString();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRoles(userService.getRoles(username));
        simpleAuthorizationInfo.addStringPermissions(userService.getPermissions(username));
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String token = (String) authenticationToken.getCredentials();    
        String userName = null;
        try {
            userName = JwtUtil.getClaimField(token, JwtUtil.ACCOUNT);
            User user = userService.getUserByName(userName);
            if (user == null) {
                System.out.println("用户不存在");
                return null;
            }

            boolean verify = JwtUtil.verify(token, userName, user.getPassword());
            if (!verify) {
                System.out.println("Token校验不正确");
                return null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                userName,token,getName());
        return authenticationInfo;
    }
}

在类JwtRealm 中,doGetAuthenticationInfo用于校验认证,而doGetAuthorizationInfo用于返回用户的权限。至于具体的查看用户是否存在和获取用户权限部分,放在UserService中实现。

6、创建JwtSubjectFactory ,关闭session:

public class JwtSubjectFactory extends DefaultWebSubjectFactory {
    @Override
    public Subject createSubject(SubjectContext context) {
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}

7、创建ShiroConfig进行配置,这里除了配置shiro自身三个核心组件filterrealmsecurityManager的注入外,还关闭了shiro的会话管理,注入Subject工厂,以及开启对shiro注解的支持:

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager webSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(webSecurityManager);

        Map<String,String> filterChainDefinitionMap=new LinkedHashMap<>();
        filterChainDefinitionMap.put("/toLogin","anon");
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSuccessUrl("/index");

        //shiro自定义过滤器
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filters);
        filterChainDefinitionMap.put("/**","jwt");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager defaultSessionManager = new DefaultWebSessionManager();
        defaultSessionManager.setSessionValidationSchedulerEnabled(false);
        return defaultSessionManager;
    }

    @Bean
    public DefaultWebSubjectFactory subjectFactory() {
        return new JwtSubjectFactory();
    }

    @Bean(name = "defaultWebSecurityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("realm") JwtRealm realm,
                     SubjectFactory subjectFactory, SessionManager sessionManager){
        DefaultWebSecurityManager webSecurityManager=new DefaultWebSecurityManager();
        webSecurityManager.setRealm(realm);

        //关闭shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        webSecurityManager.setSubjectDAO(subjectDAO);

        webSecurityManager.setSubjectFactory(subjectFactory);
        webSecurityManager.setSessionManager(sessionManager);
        return webSecurityManager;
    }

    @Bean(name = "realm")
    public JwtRealm myRealm(){
        return new JwtRealm();
    }

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

    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

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

需要注意,不要通过注解的方式直接把自定义的JwtFilter注入到spring容器中,而是在ShiroFilter中注入JwtFilter并使用它拦截一切非匿名访问的请求。

8、实现登录逻辑,验证密码是否正确,登录成功后返回签发的token:

@Controller
public class LoginController {
    @Autowired
    UserService userService;

    @ResponseBody
    @PostMapping(value = "toLogin")
    public Result<JSONObject> login(String username, String password) {
        Result<JSONObject> result = new Result<>();
        JSONObject json = new JSONObject();

        User user = userService.getUserByName(username);
        if (user == null) {
            json.put("error", "用户不存在");
            result.setData(json);
            return result;
        }

        if (!user.getPassword().equals(password)) {
            json.put("error", "密码不正确");
            result.setData(json);
            return result;
        }

        String token = JwtUtil.sign(username, password);
        json.put("token", token);
        result.setData(json);
        return result;
    }
}

9、定义测试接口,并定义接口访问权限:

@RestController
public class TestController {
   @GetMapping("test1")
   @RequiresRoles("admin")
   @RequiresPermissions("user:add")
   public String test(){
       return "test1";
   }

   @GetMapping("test2")
   @RequiresPermissions("other:copy")
   public String test2(){
       return "test2";
   }

   @GetMapping("test3")
   @RequiresRoles("guest")
   @RequiresPermissions("other:check")
   public String test3(){
       return "test3";
   }
}

10、使用postman进行测试,登录成功后会返回token:

调用接口时携带刚才返回的token,调用成功并返回结果:

访问没有权限的接口,shiro会对其进行拦截:

到这里,简单的shiro整合jwt就完成了。

最后

作者简介,码农参上,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术。个人微信DrHydra9,欢迎添加好友,进一步交流。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。