Shiro整合JWT实战

简介: Shiro整合JWT实战

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

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

JWT实现认证思路:

因为要实现无状态,所以jwt需要关闭shirosession管理

用户第一次登录成功时,使用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);
    }
}

verify方法中,根据密码生成jwt校验器,校验token是否正确

getClaimField方法用于获得token中指定字段的信息

sign方法用于生成附带用户信息及过期时间的签名

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方法。

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;
    }
}

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

image.png

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

image.png

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

image.png

相关文章
|
4月前
|
安全 Java 应用服务中间件
Shiro + JWT 进行登录验证
Shiro + JWT 进行登录验证
47 2
|
3月前
|
存储 缓存 数据库
【万字长文】微服务整合Shiro+Jwt,源码分析鉴权实战
介绍如何整合Spring Boot、Shiro和Jwt,以实现一个支持RBAC的无状态认证系统。通过生成JWT token,实现用户无状态登录,并能根据用户角色动态鉴权,而非使用Shiro提供的注解,将角色和权限信息硬编码。此外,文章还探讨了如何对Shiro的异常进行统一捕获和处理。作为应届生,笔者在学习Shiro的过程中进行了一些源码分析,尽管可能存在不足和Bug,但希望能为同样需要实现权限管理的开发者提供参考,并欢迎各位大佬指正完善。
300 65
【万字长文】微服务整合Shiro+Jwt,源码分析鉴权实战
|
1月前
|
JSON 数据安全/隐私保护 数据格式
Nest.js 实战 (八):基于 JWT 的路由身份认证鉴权
这篇文章介绍了身份验证的重要性和多种处理策略,重点放在了JWT(JSON Web Token)认证在Nest.js框架中的应用。文章包含了JWT认证的流程,如何在Nest.js中实现,以及如何创建JWT认证策略。包括了安装依赖,创建处理认证流程的文件,以及如何使用HttpException过滤器来处理未登录访问。
Nest.js 实战 (八):基于 JWT 的路由身份认证鉴权
|
1月前
|
NoSQL 安全 Java
Java Spring Boot中使用Shiro、JWT和Redis实现用户登录鉴权
Java Spring Boot中使用Shiro、JWT和Redis实现用户登录鉴权
|
4月前
|
安全 关系型数据库 MySQL
node实战——后端koa结合jwt连接mysql实现权限登录(node后端就业储备知识)
node实战——后端koa结合jwt连接mysql实现权限登录(node后端就业储备知识)
74 3
|
4月前
|
安全 算法 Java
SpringBoot+JWT+Shiro+MybatisPlus实现Restful快速开发后端脚手架
SpringBoot+JWT+Shiro+MybatisPlus实现Restful快速开发后端脚手架
201 0
|
4月前
|
开发框架 安全 Java
【Java专题_01】springboot+Shiro+Jwt整合方案
【Java专题_01】springboot+Shiro+Jwt整合方案
94 0
|
4月前
|
存储 Java 数据库
SpringBoot+JWT+Shiro
SpringBoot+JWT+Shiro
48 0
|
4月前
|
Java
Springboot整合之Shiro和JWT技术实现无感刷新9
Springboot整合之Shiro和JWT技术实现无感刷新9
|
4月前
|
JSON 安全 Java
Springboot整合之Shiro和JWT技术实现无感刷新8
Springboot整合之Shiro和JWT技术实现无感刷新8

热门文章

最新文章