Shiro-Jwt整合

在这里插入图片描述

Shiro-Jwt整合

Github项目地址:https://github.com/realLiuzh/ShiroJwt-Verification

特性

  • SpringBoot整合Shiro-Jwt实现RBAC(Role-Based-Access-Control)。
  • 整合腾讯云SMS服务,实现手机号登录注册。
  • Validation实现参数校验。
  • 跨域支持。

前置知识

接口文档

https://www.showdoc.com.cn/1772458942730902/8234809986254447

准备Maven工程

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.lzh</groupId>
    <artifactId>ShiroJwt-Verification</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ShiroJwt-Verification</name>
    <description>ShiroJwt-Verification</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    </properties>

    <dependencies>

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--腾讯云-->
        <dependency>
            <groupId>com.tencentcloudapi</groupId>
            <artifactId>tencentcloud-sdk-java</artifactId>
            <version>3.1.270</version><!-- 注:这里只是示例版本号(可直接使用),可获取并替换为 最新的版本号,注意不要使用4.0.x版本(非最新版本) -->
        </dependency>

        <!--加密解密类-->
        <dependency>
            <groupId>org.jasypt</groupId>
            <artifactId>jasypt</artifactId>
            <version>1.9.3</version>
        </dependency>

        <!--validator-->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>5.3.6.Final</version>
        </dependency>

        <!--jwt-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.12.0</version>
        </dependency>

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>

        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <!--shiro-spring-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.7.0</version>
        </dependency>

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.7.RELEASE</version>
                <configuration>
                    <mainClass>com.lzh.ShiroJwtVerificationApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

JwtUtil

package com.lzh.util;

/**
 * Jwt工具类
 *
 * @author 志昊的刘
 * @date 2021/12/21
 */
public class JwtUtil {

    /**
     * 生成token
     * claim:
     * username -> RealUsername
     * exp      -> 有效时间5天
     * 加密算法   -> HMAC256
     */
    public static String getToken(String username) {
        //username + 私钥 = 最终的secret
        Algorithm algorithm = Algorithm.HMAC256(username + Constant.JWT_PRIVATE_KEY);
        return JWT.create()
                //jwt payload
                .withClaim(Constant.JWT_CLAIM_KEY, username)
                //jwt 有效时间
                .withExpiresAt(new Date(System.currentTimeMillis() + Long.parseLong(Constant.JWT_EXPIRE_TIME) * 1000))
                .sign(algorithm);
    }

    /**
     * 检验token
     */
    public static void verifyToken(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(getClaim(token, Constant.JWT_CLAIM_KEY) + Constant.JWT_PRIVATE_KEY);
            JWTVerifier jwtVerifier = JWT.require(algorithm).build();
            jwtVerifier.verify(token);
        } catch (Exception e) {
            e.printStackTrace();
            throw new AuthenticationException();
        }
    }

    /**
     * 获得Token中的荷载信息  (无需解码)
     */
    public static String getClaim(String token, String claimKey) {
        return JWT.decode(token).getClaim(claimKey).asString();
    }
}

Shiro组件

JwtToken

package com.lzh.data;


/**
 * 自定义JwtToken
 *
 * @author 志昊的刘
 * @date 2021/12/21
 */

@AllArgsConstructor
public class JwtToken implements AuthenticationToken {

    private String token;

    /**
     * 注意:实现的两个方法全部返回token。(对于理解ShiroJwt整合很重要)
     */
    @Override
    public Object getPrincipal() {
        return token;
    }

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

JwtFilter

package com.lzh.config.filter;

/**
 * Jwt过滤器
 *
 * @author 志昊的刘
 * @date 2021/12/21
 */
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
    /**
     * 过滤器执行流程:
     * isAccessAllowed()->isLoginAttempt()->executeLogin()
     */

    /**
     * 是否允许访问
     * 逻辑:只有同时满足以下条件才允许该请求访问:
     * 1、请求头中携带token;
     * 2、token经过检验合格(通过自定义Realm的认证);
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                return false;
            }
        }
        return false;
    }

    /**
     * 是否有认证意愿
     * 逻辑:如果请求头中如有token则认为有认证意愿  (一定要把认证和登录概念区分开来)
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader(Constant.HTTP_HEADER_TOKEN);
        return token != null;
    }

    /**
     * 执行认证
     * 逻辑:在HttpHeader中获取前端传来的token,并将其封装为JwtToken。然后将JwtToken传给自定义Realm进行认证
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader(Constant.HTTP_HEADER_TOKEN);
        JwtToken jwtToken = new JwtToken(token);
        //提交给realm进行认证,如果发生错误,会在realm中抛出异常并捕获
        getSubject(request, response).login(jwtToken);
        //如果没有抛出异常则证明认证成功,返回true
        return true;
    }

    /**
     * 猜测:如果isAccessAllowed()返回false会调用sendChallenge()方法
     * sendChallenge()调用super.Challenge()方法 访问接口会返回401错误
     */
    @Override
    protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
        log.debug("Authentication required: sending 401 Authentication challenge response.");
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        httpResponse.setCharacterEncoding("utf-8");
        httpResponse.setContentType("application/json;charset=utf-8");
        Response responseData = new Response();
        responseData.setCode(CustomExceptionType.NO_LOGIN.getCode());
        responseData.setMessage(CustomExceptionType.NO_LOGIN.getTypeDesc());

        PrintWriter out = null;
        try {
            out = httpResponse.getWriter();
        } catch (IOException e) {
            e.printStackTrace();
        }
        String json = JSON.toJSONString(responseData);
        assert out != null;
        out.print(json);
        out.flush();

        return false;
    }
}

CustomRealm

package com.lzh.config.shiro;

/**
 * 自定义Realm
 *
 * @author 志昊的刘
 * @date 2021/12/21
 */
public class CustomRealm extends AuthorizingRealm {


    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleMapper roleMapper;


    /**
     * 大坑,必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken authenticationToken) {
        return authenticationToken instanceof JwtToken;
    }

    /**
     * 认证
     * 这里认证并非真正的登录认证,shiro与jwt整合,登录还是由Service层负责
     * 而这里的认证承担的是校验Token的责任
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        JwtToken jwtToken = (JwtToken) authenticationToken;
        String token = (String) jwtToken.getPrincipal();

        JwtUtil.verifyToken(token);

        String username = JwtUtil.getClaim(token, Constant.JWT_CLAIM_KEY);
        /**
         * Q:这里的认证异常 AuthenticationException 抛给了谁?
         * A:层层调用后,抛给了JwtFilter的isAccessAllowed()方法 最终结果是isAccessAllowed()返回false。
         * Q:异常信息是什么?
         * A:异常信息是sendChallenge()中写死的方法,这里(包括JwtUtil中)抛出的认证异常仅仅起标识作用
         */
        if (username == null) {
            throw new AuthenticationException();
        }
        User user = userMapper.selectByUsername(username);

        if (user == null) {
            throw new AuthenticationException();
        }
        /**
         * param1:真实的用户名
         * param2:真实的密码
         * param3:该realm名称
         *
         * Shiro将param2与token中的credentials比较,如果相等则验证通过
         * 在这里比较结果恒为true
         */
        return new SimpleAuthenticationInfo(token, token, this.getName());
    }


    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        String username = JwtUtil.getClaim(principalCollection.toString(), Constant.JWT_CLAIM_KEY);

        UserDto userDto = userMapper.selectRoleByUserName(username);

        if (userDto.getRole() == null)
            return simpleAuthorizationInfo;

        RoleDto roleDto = roleMapper.selectPermissionByRoleId(userDto.getRole().getId());

        /**
         * 这里模拟的用户只有一个身份
         * 有多个身份调用addRoles()
         */
        simpleAuthorizationInfo.addRole(userDto.getRole().getName());
        simpleAuthorizationInfo.addStringPermission(roleDto.getPermission().getPerCode());
        return simpleAuthorizationInfo;
    }


}

ShiroConfig

package com.lzh.config.shiro;

/**
 * shiro配置类
 *
 * @author 志昊的刘
 * @date 2021/12/21
 */
@Configuration
public class ShiroConfig {

    /**
     * 将realm注入容器中
     */
    @Bean
    public CustomRealm customRealm() {
        return new CustomRealm();
    }

    /**
     * 使得securityManager生效
     */
    @Bean
    public DefaultWebSecurityManager manager(@Qualifier("customRealm") CustomRealm customRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        //关联Realm(使CustomRealm生效)
        manager.setRealm(customRealm);

        //关闭Shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);
        return manager;
    }

    /**
     * 重要:配置过滤器和拦截路径
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("manager") DefaultWebSecurityManager manager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        factoryBean.setSecurityManager(manager);

        //添加过滤器
        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwtFilter", new JwtFilter());
        factoryBean.setFilters(filters);

        //设置拦截路径 (与JwtFilter配合食用)
        Map<String, String> map = new HashMap<>();
        map.put("/user/usernameLogin", "anon");
        map.put("/user/phoneLogin", "anon");
        map.put("/user/register", "anon");
        map.put("/user/sendVerCode","anon");
        map.put("/**", "jwtFilter");

        factoryBean.setFilterChainDefinitionMap(map);

        //factoryBean.setUnauthorizedUrl("/user/unauth");
        return factoryBean;
    }


    /**
     * 下面的代码是添加注解支持
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

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

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("manager") DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }


}

其他一些工具类

加密工具类

package com.lzh.util;


/**
 * 加密工具类
 *
 * @author 志昊的刘
 * @date 2021/12/21
 */
public class EncryptUtil {

    /**
     * sh1加密方法加密
     */
    public static String encryptor(String input) {
        BasicPasswordEncryptor basicPasswordEncryptor = new BasicPasswordEncryptor();
        return basicPasswordEncryptor.encryptPassword(input);
    }

    /**
     * 检查密码的合法性
     */
    public static void checkPassword(String inputPassword, String realPassword) {
        BasicPasswordEncryptor passwordEncryptor = new BasicPasswordEncryptor();
        AssertUtil.isTrue(!passwordEncryptor.checkPassword(inputPassword, realPassword), CustomExceptionType.ACCOUNT_ERROR);
    }
}

短信工具类

package com.lzh.util;

import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20190711.SmsClient;
import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse;

import static com.lzh.data.Constant.*;

/**
 * 短信发送工具类
 *
 * @author 志昊的刘
 * @date 2021/12/24
 */
public class SmsUtil {

    public static boolean sendSms(String phone, String captcha) {
        try {

            Credential cred = new Credential(SMS_SECRET_ID, SMS_SECRET_KEY);

            HttpProfile httpProfile = new HttpProfile();

            httpProfile.setConnTimeout(60);
            httpProfile.setEndpoint("sms.tencentcloudapi.com");

            ClientProfile clientProfile = new ClientProfile();
            clientProfile.setSignMethod("HmacSHA256");
            clientProfile.setHttpProfile(httpProfile);
            SmsClient client = new SmsClient(cred, "ap-guangzhou", clientProfile);
            // 实例化一个请求对象
            SendSmsRequest req = new SendSmsRequest();

            /**
             * 填充请求参数,这里request对象的成员变量即对应接口的入参
             */

            /* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */
            req.setSmsSdkAppid(SMS_SDK_APP_ID);

            /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,签名信息可登录 [短信控制台] 查看 */
            req.setSign(SMS_SIGN_NAME);

            /* 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看 */
            req.setTemplateID(SMS_TEMPLATE_ID);

            /* 下发手机号码,采用 e.164 标准,+[国家或地区码][手机号]
             * 例如+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号*/
            phone="86"+phone;
            String[] phoneNumbers = {phone};
            req.setPhoneNumberSet(phoneNumbers);

            /* 模板参数: 若无模板参数,则设置为空 */
            String[] templateParamSet = {captcha};
            req.setTemplateParamSet(templateParamSet);

            /* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的
             * 返回的 res 是一个 SendSmsResponse 类的实例,与请求对象对应 */
            SendSmsResponse res = client.SendSms(req);

            // 输出json格式的字符串回包
            System.out.println(SendSmsResponse.toJsonString(res));

            // 也可以取出单个值,你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义
            System.out.println(res.getRequestId());


        } catch (TencentCloudSDKException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Shiro 中整合 JWT(JSON Web Token),可以实现基于令牌的身份验证和授权机制。下面是一个简单的示例代码,演示了如何在 Shiro 中使用 JWT: 首先,需要创建一个 JWTRealm 类,继承自 Shiro 的 AuthenticatingRealm 类。在 JWTRealm 中,我们可以通过重写 doGetAuthenticationInfo 和 doGetAuthorizationInfo 方法来实现身份验证和授权逻辑。 ```java public class JWTRealm extends AuthenticatingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { JWTToken jwtToken = (JWTToken) authenticationToken; String token = jwtToken.getToken(); // 解析 JWT Token,获取用户名 String username = JWTUtil.getUsername(token); // 根据用户名查询数据库或其他存储,获取用户信息 User user = userService.findByUsername(username); // 如果用户不存在,抛出 UnknownAccountException 异常 if (user == null) { throw new UnknownAccountException("用户不存在"); } // 验证 Token 是否有效 if (!JWTUtil.verify(token, username, user.getPassword())) { throw new AuthenticationException("Token 验证失败"); } // 构造 SimpleAuthenticationInfo 对象,并返回 return new SimpleAuthenticationInfo(user, token, getName()); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 从 PrincipalCollection 中获取当前用户信息 User user = (User) principalCollection.getPrimaryPrincipal(); // 查询用户的角色和权限信息 Set<String> roles = userService.findRolesByUsername(user.getUsername()); Set<String> permissions = userService.findPermissionsByUsername(user.getUsername()); // 构造 SimpleAuthorizationInfo 对象,并设置角色和权限信息 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(permissions); return authorizationInfo; } } ``` 然后,在 Shiro 的配置文件中,需要将 JWTRealm 注册为一个 Realm,并关闭默认的 Session 和 Cookie 管理: ```xml <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="jwtRealm" /> </bean> <bean id="jwtRealm" class="com.example.JWTRealm" /> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager" /> <property name="arguments" ref="securityManager" /> </bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- ... --> <property name="securityManager" ref="securityManager" /> <!-- ... --> </bean> ``` 最后,需要在登录接口处生成 JWT Token,并返回给前端: ```java @PostMapping("/login") public Map<String, Object> login(@RequestBody Map<String, String> params) { String username = params.get("username"); String password = params.get("password"); // 验证用户名和密码 if (userService.verify(username, password)) { // 生成 JWT Token String token = JWTUtil.sign(username, password); // 将 Token 返回给前端 Map<String, Object> resultMap = new HashMap<>(); resultMap.put("token", token); return resultMap; } else { throw new AuthenticationException("用户名或密码错误"); } } ``` 以上代码只是一个简单的示例,实际应用中可能还需要进行一些优化和增强。希望能对你的 Shiro 整合 JWT 有所帮助!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值