测试平台开发(一)鉴权模块7 Shiro基于JWT的认证

Shiro简介

Apache Shiro 是一个强大且易用的 Java 安全框架,主要用于身份认证、授权、加密和会话管理。它的设计目标是简化安全性的实现,使开发者能够更专注于业务逻辑。以下是 Shiro 的主要作用和功能:

1. 身份认证(Authentication)
用户登录:Shiro 提供了简单而强大的 API 来处理用户登录。你可以通过 Subject 对象调用 login 方法来验证用户的身份。
多方式认证:Shiro 支持多种形式的认证,包括用户名/密码、证书、OpenID 等。
自定义认证:你可以通过实现自定义的 Realm 来扩展 Shiro 的认证功能,以适应不同的认证需求。
2. 授权(Authorization)
权限管理:Shiro 提供了细粒度的权限管理功能,可以基于角色、权限字符串等方式进行授权。
访问控制:Shiro 支持 URL 访问控制、方法级别的访问控制等,可以灵活地控制用户对资源的访问。
注解支持:Shiro 提供了注解支持,如 @RequiresRoles、@RequiresPermissions 等,可以在代码中方便地进行权限检查。
3. 会话管理(Session Management)
无状态会话:Shiro 支持无状态会话,适用于分布式系统和无状态应用,如使用 JWT 进行身份验证。
会话集群:Shiro 可以将会话信息存储在集群中,确保会话的一致性和可用性。
会话监听:Shiro 提供了会话监听器,可以监控会话的创建、更新和销毁事件。
4. 加密(Cryptography)
密码加密:Shiro 提供了多种密码加密算法,如 SHA-256、MD5 等,确保用户密码的安全。
对称加密:Shiro 支持对称加密算法,如 AES,用于保护敏感数据。
哈希和盐:Shiro 提供了哈希和盐的功能,可以增强密码的安全性。
5. Web 支持
过滤器:Shiro 提供了 ShiroFilter,可以轻松地集成到 Web 应用中,进行 URL 访问控制。
标签库:Shiro 提供了 JSP 标签库,可以在页面中进行权限检查和显示不同的内容

1、引入shiro的maven依赖包

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.2.5</version>
            <scope>compile</scope>
            <exclusions>
                <exclusion>
                    <artifactId>commons-beanutils</artifactId>
                    <groupId>commons-beanutils</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.2.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.2.5</version>
        </dependency>

2、使用token自动登录

1)使用身份验证过滤器BasicHttpAuthenticationFilter,验证用户是否为登录状态,接口调用时生效;

      如果为否执行登录,登录失败会返回401错误。

public class JWTFilter extends BasicHttpAuthenticationFilter {
    /**
     * 检查用户是否登录
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        // 验证请求头中是否存在Authorization字段,不存在则返回false
        String header = req.getHeader(SecurityUtils.REQUEST_AUTH_HEADER);
        return header != null;
    }
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String header = req.getHeader(SecurityUtils.REQUEST_AUTH_HEADER);
        // 执行登录时需要传入token,因此要传入一个AuthenticationToken对象
        JWTTokenDto jwtTokenDto = new JWTTokenDto();
        jwtTokenDto.setToken(header);
        getSubject(request,response).login(jwtTokenDto);
        return true;
    }
    /**
     * 是否允许登录
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        boolean loginAttempt = isLoginAttempt(request, response);
        if (loginAttempt){
            executeLogin(request,response);
            return true;
        }
        // 如果未登录状态,则返回401错误
        response401(response);
        return true;
    }
    private Boolean response401(ServletResponse response){
        HttpServletResponse res = (HttpServletResponse) response;
        res.setStatus(HttpStatus.UNAUTHORIZED.value());
        res.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = null;
        try {
            out = res.getWriter();
            out.append(JSON.toJSONString(ResponseFactory.getError(ResponseCode.please_login)));
            out.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            if (out != null){
                out.close();
            }
        }
        return false;
    }
}

2)登录的方式是使用token进行登录,因此需要定义token类

public class JWTTokenDto implements AuthenticationToken {
    private String token;

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

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

3、配置shiro拦截规则

shiro规则使用@Configuration注解,作为组件在启动时配置,加载以下三个部件:

①shiroFilter定义的URI过滤规则

②securityManager定义的安全会话规则

③指定JWTRealm()实现的鉴权规则

@Configuration
public class ShiroConfig {
    /**
     * 定义权限,启动时加载shiro拦截器
     * @Qualifier注解用来指定需要的bean
     * @param manager
     * @return
     */
    @Bean(name="shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager")SecurityManager manager){
        // 定义拦截规则,此处加载了https的规则、以及JWT使用token进行验证的规则
        Map<String, Filter> filterMap = new HashMap<>();
        // https的规则,定义过滤器名称为ssl
        filterMap.put("ssl",new SslFilter());
        // 配置规则名称为jwt,定义过滤器名称为jwt
        filterMap.put("jwt",new JWTFilter());

        // 设定不需要拦截的规则,加入到这个规则的URI不会拦截
        LinkedHashMap<String,String> filterRules = new LinkedHashMap<>();
        // 匹配规则:模糊匹配用**,anon代表匿名访问,用anon标记规则不需要身份验证
        filterRules.put("/swagger**","anon");
        filterRules.put("/swagger-ui/**","anon");
        // 把登录接口驾到过滤规则中,不然登录也会提示401
        filterRules.put("/api/auth/tokens","anon");
        filterRules.put("/api/auth/regist","anon");
        filterRules.put("/api/auth/401","anon");
        // 设置需要拦截的规则,所有接口都会被拦截,拦截规则为jwt规则,即前面定义的JWTFilter()
        filterRules.put("/**","jwt");

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setFilters(filterMap);
        // 设置安全事务管理器,使用@Qualifier("securityManager")进行了指定
        shiroFilterFactoryBean.setSecurityManager(manager);
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRules);
        // 设置login地址
        shiroFilterFactoryBean.setLoginUrl("/course/login");
        // 设置没有登录的地址
        shiroFilterFactoryBean.setUnauthorizedUrl("/course/401");
        return shiroFilterFactoryBean;
    }

    // 配置核心安全事务管理器
    @Bean(name = "securityManager")
    public SecurityManager securityManager(){
        // 禁用会话存储,意味着shiro不会把用户的会话信息存储在内存中
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        // 设置SubjectDAO禁用会话存储,Subject代表当前安全上下文,DAO代码访问安全信息的接口
        DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
        defaultSubjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setSubjectDAO(defaultSubjectDAO);
        // jwtRealm用于处理JWT的认证和授权,setRealm后会让shiro用该realm进行身份认证
        manager.setRealm(jwtRealm());
        return manager;
    }

    @Bean(name = "jwtRealm")
    public JWTRealm jwtRealm(){
        JWTRealm jwtRealm = new JWTRealm();
        return jwtRealm;
    }
}

4、配置AuthorizingRealm鉴权规则

AuthorizingRealm中的doGetAuthenticationInfo,主要用途是实现具体的身份验证逻辑。当用户尝试登录或访问需要身份验证的资源时,框架会调用这个方法来验证用户的身份。

supports则用来定义doGetAuthenticationInfo是否执行,supports返回true则执行,否则不执行。

public class JWTRealm extends AuthorizingRealm {
    @Autowired
    private UserServiceImp userService;

    @Override
    public boolean supports(AuthenticationToken token) {
        // token是否属于JWTTokenDto,属于则启用下面的鉴权规则
        return token instanceof JWTTokenDto;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 从uri中取出token
        String token = (String)authenticationToken.getCredentials();
        // 从token中取出username
        String userAccount = JWTUtils.getUserAccount(token);
        // 从数据库查询该用户名是否存在
        UserEntity user = userService.getUserByAccount(userAccount);
        if (user == null){
            throw new ServiceException(ResponseCode.please_login);
        }
        // 判断用户状态是否已经停用
        if ("-1".equals(String.valueOf(user.getUserStatus()))){
            throw new ServiceException(ExceptionCodeEnum.user_disable);
        }
        // 判断用户名密码错误
        if (!JWTUtils.verify(token,userAccount,user.getPassword())){
            throw new ServiceException(ExceptionCodeEnum.user_password_error);
        }

        SimpleAuthenticationInfo jwtRealm = new SimpleAuthenticationInfo(token, token, "JwtRealm");
        return jwtRealm;
    }
}

代码中调用了JWTUtils.getUserAccount、JWTUtils.verify,代码如下:

    public static String getUserAccount(String token){
        try {
            // 解码token获得用户名
            DecodedJWT decode = JWT.decode(token);
            // claim是token中的声明信息
            return decode.getClaim(SecurityUtils.ACCOUNT).asString();
        } catch (JWTDecodeException exception){
            // 解析失败,返回空
            return null;
        }
    }

    public static boolean verify(String token,String account,String pwd){
        try{
            // 使用HMAC256算法创建一个加密算法对象,密钥为传入的密码pwd。
            Algorithm algorithm = Algorithm.HMAC256(pwd);
            // 使用当前用户名和算法对象,构建一个JWT验证器,用来验证传入的token
            JWTVerifier jwtVerifier = JWT.require(algorithm).withClaim(SecurityUtils.ACCOUNT, account).build();
            // verify(token)方法会检查令牌的签名、过期时间以及声明(Claim)
            // 如果一切正常,会返回一个DecodedJWT对象,如果验证失败,会抛出相应的异常。
            DecodedJWT verify = jwtVerifier.verify(token);
            return true;
        }catch (Exception e){
            return false;
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值