渐进式 shiro - shiro + jwt+salt (三)

渐进式-springboot-shiro-jwt

完整版:springboot + shiro + jwt + salt.

放弃 Cookie ,Session ,使用 JWT 进行鉴权,完全实现无状态鉴权

shiro 完整流程以及集成:

  1. 用户访问登录接口 /login, 用户输入登录账号和密码被封装成 UsernamePasswordToken 对象,然后调用 subject.login() 方法
  2. shiro 立即进入用户认证过程,进入执行 UserRealm doGetAuthenticationInfo()方法代码块。
  3. 用户登录成功后,登录接口返回生成得 token.
  4. 访问其他所有需要携带token得接口,此处以支付接口 /pay为例子,必须用户登录成功访问并在请求头中添加 token。
  5. 控制哪些请求需要在请求头中添加 token,哪些请求不需要 token 可以直接访问(比如/login)的方式叫做:jwt(JSON Web Token),无状态鉴权机制
@Slf4j
@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/login")
    public AjaxResult loginUser(@RequestBody UserEntity userVo) {
        Subject subject = SecurityUtils.getSubject();

        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userVo.getUsername(), userVo.getPassword());
        usernamePasswordToken.setRememberMe(true);

        try {
            subject.login(usernamePasswordToken);
            log.info("登录成功");
        } catch (AuthenticationException ae) {
            return AjaxResult.error("账号或密码不正确");
        }

        UserEntity userEntity = (UserEntity) subject.getPrincipal();
        userEntity.setToken(JwtUtils.generateToken(userEntity.getUsername(),JwtUtils.secret));

        return AjaxResult.success(userEntity);

    }

    @GetMapping("/pay")
    public AjaxResult payWithToken() {
        return AjaxResult.success("this Uri need token");
    }
}

引入依赖

JWT 方案有许多种,这个网站列举了所有的常用方案,在这里我们选择 java-jwt

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.10.0</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>
<!--    jwt工具    -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.2.0</version>
</dependency>

ShiroConfig 配置类

对于 HTTP 请求,springboot 默认使用 Servlet 来处理,而 shiro 的过滤器正是基于 Servlet 实现,因此所有的 Http 请求,都会执行设定好的过滤器方法.

在前面的文章中,其实我们已经使用了过滤器,使用的都是 shiro 提供的现成过滤器名称缩写:shiro 常见过滤器

在 ShiroConfig ShiroFilterFactoryBean 中, 对过滤器进行统一的设定.代码变动位置有 2 处

@Configuration
public class ShiroConfig {

    /**
     * 默认web安全管理器
     *
     * @return {@link DefaultWebSecurityManager}
     */
        @Bean
    public SessionManager sessionManager(){
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        // 关闭 cookie 验证
        sessionManager.setSessionIdCookieEnabled(false);
        // 关闭 session 验证
        sessionManager.setSessionValidationSchedulerEnabled(false);
        return sessionManager;
    }

    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        securityManager.setRealm(userRealm());
        securityManager.setSessionManager(sessionManager());

        /*
         * 关闭shiro自带的session
         * 文档: http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    /**
     * `shiroFilter`:过滤器
     *
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // Shiro的核心安全接口,这个属性是必须的
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 自定义过滤器 ================ 变动1
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("myFilter", new MyFilter());
        shiroFilterFactoryBean.setFilters(filters);

        // 定义过滤链
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 对静态资源设置匿名访问
        filterChainDefinitionMap.put("/index.html", "anon");
        filterChainDefinitionMap.put("/favicon.ico**", "anon");
        filterChainDefinitionMap.put("/static/**","anon");

        // 登录,不需要拦截的访问
        filterChainDefinitionMap.put("/login", "anon");
        // 错误页面无需认证
        filterChainDefinitionMap.put("/error","anon");


        // !!! 其他所有请求使用自定义的过滤器 myFilter 来处理  ================ 变动2
        filterChainDefinitionMap.put("/**","myFilter")
        // filterChainDefinitionMap.put("/**","authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

}

filterChainDefinitionMap.put("/",“myFilter”)** 用来设置其他所有请求包括 /pay 会执行即将创建的自定义过滤器 myFilter

自定义 UserRealm

在 ShiroConfig 中,规定了只有登录接口(subjet.login())会使用 UserRealm.需要携带token得接口与UserRealm 毫无关系

ShiroRealm 设计为可拔插模块,而 Realm 又分为两部分:认证,授权。两个单词非常相似。

  • 授权 doGetAuthorizationInfo: 处理角色是否能够访问相应的 web service 相关信息
  • 认证 doGetAuthenticationInfo: 处理角色登录相关信息
public class UserRealm extends AuthorizingRealm {

    /**
     * shiro默认机制是 通过token的类型来确认是否由当前realm来处理当前收到的登录请求
     * 因此 在这里限定只有通过 UsernamePasswordToken这个类,调用的login接口可以使用此Realm认证
     * 
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
    /**
     * 认证
     * AuthenticationToken 接口提供了2方法,getPrincipal() 返回的用户的账号信息,getCredentials() 返回的是密码信息。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 登陆时传入的用户名,密码
        UsernamePasswordToken accessToken = (UsernamePasswordToken) authenticationToken;
        // 获取用户名
        String username = (String) authenticationToken.getPrincipal();

        // 查询用户
        UserEntity userEntity = userService.getOne(new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getUsername, accessToken.getUsername()));

        // 组装并返回
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                userEntity, // 用户
                accessToken.getPassword(), // 密码
                ByteSource.Util.bytes(salt),// byte类型 salt
                "anyRealmName"  // realm name .  getName()
        );
        return authenticationInfo;
    }
}

自定义过滤器 MyFilter

当访问 /pay时,请求会执行 MyFilter 中代码块。需要注意的是,当你修改了代码后,要先执行以下 /login 登录,复制返回得 token,再访问此接口


/**
 * $$ 代码的执行流程 preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin
 *
 * @author ifredomvip@gmail.com
 * @version 1.0.0
 * @date 2022/10/27 17:18
 **/
@Slf4j
public class MyFilter extends BasicHttpAuthenticationFilter {
    /**
     * 过滤器拦截请求的入口方法,所有请求都会进入该方法
     * 1. 返回true则允许访问
     * 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
     * @param request     请求
     * @param response    响应
     * @param mappedValue 映射值
     * @return boolean
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        log.info("允许访问 - 周期");
        // 所有自定义过滤器得请求都需要携带token
        return false;
    }

    /**
     * isAccessAllowed()方法返回false,会进入该方法,表示拒绝访问
     *
     * 所有过滤器处理请求都可以在 isAccessAllowed中处理,或者在 onAccessDenied 中处理
     *
     * 由于过滤器在controller前运行,token过期时,抛出的异常不会全局异常捕获,而在 onAccessDenied 是可以精准抛出此异常。
     *
     * 所以在 onAccessDenied 中处理
     *
     * @param request  请求
     * @param response 响应
     * @return boolean
     * @throws Exception 异常
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        log.info("拒绝访问 - 周期");
        //获取请求token,如果token不存在,直接返回401
        String token = getRequestToken((HttpServletRequest) request);

        // 请求头不含token
        if(StringUtils.isEmpty(token)) {
            responseError(response,HttpStatus.UNAUTHORIZED.value(),"token不能为空");
            return false;
        }
        // 请求头含有 token
        String username = JwtUtils.getUserName(token);
        if(!JwtUtils.verify(token, username, JwtUtils.secret)){
            responseError(response,HttpStatus.UNAUTHORIZED.value(),"token无效");
            return false;
        }

        if(JwtUtils.isExpired(token)) {
            responseError(response,HttpStatus.UNAUTHORIZED.value(),"token已失效,请重新登录!");
            return false;
        }
        log.info(String.valueOf("verify"));
        return true;
    }


    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        fillCorsHeader(httpRequest,httpResponse);

        // 过滤options方法。跨域时会首先发送一个option请求
        if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpResponse.setStatus(HttpStatus.OK.value());
            return true;
        }
        return super.preHandle(request, response);
    }

    /**
     * 获取请求的token
     */
    private String getRequestToken(HttpServletRequest httpRequest) {
        //从header中获取token
        String token = httpRequest.getHeader("Authorization");

        //如果header中不存在token,则从参数中获取token
        if (StringUtils.isBlank(token)) {
            token = httpRequest.getParameter("Authorization");
        }

        return token;
    }

    /**
     * 跨域请求的解决方案之一
     *
     * @param request  请求
     * @param response 响应
     */
    protected void fillCorsHeader(HttpServletRequest request, HttpServletResponse response) {
        response.setContentType("text/html;charset=UTF-8");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        response.setHeader(
                "Access-Control-Allow-Headers",
                request.getHeader("Access-Control-Request-Headers")
        );
    }

    protected void responseError(ServletResponse response,int code,String errorMsg) throws IOException  {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        AjaxResult r = AjaxResult.error(HttpStatus.UNAUTHORIZED.value(), "token不能为空");

        String json = new ObjectMapper().writeValueAsString(r);
        httpResponse.getWriter().print(json);
    }
}

JwtUtils

工具类都定义为静态方法,使用时可以避免注入此工具类。

package com.mock.water.core.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.mock.water.modules.system.user.entity.UserEntity;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @Author ifredomvip@gmail.com
 * @Date 2022/11/9 11:14
 */
@Slf4j
public class JwtUtils {
    /**
     * 密钥
     */
    public static String secret = "ifredom123456";
    /**
     * 到期时间 7天
     */
    public static long expire = 7*1000*60*60*24;

    /**
     * 创建 token
     */
    public static String generateToken(String username, String secret) {
        Date now = new Date();
        Date date = new Date(now.getTime() + expire * 1000);
        Algorithm algorithm = Algorithm.HMAC256(secret);
        return JWT.create()
                .withClaim("username", username)
                .withExpiresAt(date)
                .sign(algorithm);
    }

    /**
     * 验证 token
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            verifier.verify(token);
            return true;
        } catch (Exception e) {
            log.error("token 无效 {}", e.getMessage());
            return false;
        }
    }

    /**
     * token是否过期
     *
     * @return true:过期
     */
    public static boolean isExpired(String token) {
        DecodedJWT jwt = JWT.decode(token);
        return System.currentTimeMillis() > jwt.getExpiresAt().getTime();
    }

    /**
     * 从 token中获取字段
     *
     * @return token中包含的填入字段
     */
    public static String getClaim(String token, String claim) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(claim).asString();
        } catch (JWTDecodeException e) {
            log.error("error:{}", e.getMessage());
            return null;
        }
    }

    public static String getUserName(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            log.error("error:{}", e.getMessage());
            return null;
        }
    }

    public String getSecret()  {return secret;}
    public void setSecret(String secret) {this.secret = secret;}
    public void setExpire(long expire) {this.expire = expire;}
    public long getExpire() {return expire;}
}

Realm 中 SimpleAuthenticationInfo 详解

在认证功能中会使用到 Shiro 封装好的 SimpleAuthenticationInfo 类.

SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
        userEntity, // 用户
        accessToken.getPassword(), // 密码
        ByteSource.Util.bytes(salt),// byte类型 salt
        "anyRealmName"  // realm name .  getName()
);
  • 第一个参数,可以传入用户名 username,也可以传入从数据库查询得到的 userEntity 实体对象。(shiro 会自动调用实体的 getUserName()去获取 username字段值)建议传入 userEntity

    传入 userEntity(实体),subject.getPrincipal() 得到的是 userEntity(实体);

    传入 username(字符串),subject.getPrincipal() 得到的是字符串。

  • 第二个参数,传入的是用户登录时输入的 password。它被装入 SimpleAuthenticationInfo类返回后,会与 UsernamePasswordToken 中的 password 进行对比。匹配上了就表明验证通过,匹配不上就报异常。

    需要注意,网上很多文章说的是传入数据库中的密码,数据库中应该存放的是 username 和 salt,或者加密之后的密码,一定不能是明文密码.

  • 第三个参数(可选参数),salt 盐。此参数目的:用于对密码进行加密以及对比,防止用户的密码相同。

    具体来说就是:假如两个用户的密码都是 123456, Shiro 在比较 数据库中获取的 passwordUsernamePasswordToken 中的 password 的值时,默认会先调用这个类 new SimpleHash(String algorithmName, Object source)对密码执行一次 MD5 哈希算法得到字符串,然后使用哈希化后的两个字符串进行比较,这两字符串相同,那么就表示密码相同。(shiro 并不会上来就直接比较 2 个密码原文,会分别哈希算法转换一次后,对比转换后的值)

    所以问题就来了, 如果两个用户密码相同,在没有 salt 的情况下,他们的哈希值是一样的,就会造成错误判断。加盐后就可以避免不同用户的密码不一样。

  • 第四个参数:当前 realm 对象的 beanName, 可以通过 getName() 获取

认证方法 doGetAuthenticationInfo 入参 AuthenticationToken 详解

认证方法 doGetAuthenticationInfo() 有一个入参,类型为 AuthenticationToken

protected AuthenticationInfo doGetAuthenticationInfo(
    AuthenticationToken authenticationToken
) throws AuthenticationException {}

参数 AuthenticationToken 是一个接口,它拥有 2 个实现类和 2 个继承接口,关系如下。

参数 authenticationToken 从哪里来呢?

它是在登陆 login() 时,我们创建一个 UsernamePasswordToken 对象然后传入的,传入的必须是一个AuthenticationToken的实现类.(经过测试,此处并不能传入 new BearerToken()这个实现类)

@PostMapping("/login")
public void loginUser(@Validated @RequestBody UserVo userVo, BindingResult bindingResult) {
    Subject subject = SecurityUtils.getSubject();

    UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userVo.getUsername(), userVo.getPassword());
    usernamePasswordToken.setRememberMe(true);

    // 传入
    subject.login(usernamePasswordToken);
}

从继承关系图可以看出,为什么它可以向下转型

UsernamePasswordToken accessToken = (UsernamePasswordToken) authenticationToken;

过滤链 ShiroFilterChainDefinition

在配置类中,shiro 提供了一个简单得封装类 ShiroFilterChainDefinition,可以将过滤连提取为一个单独得方法,代码看上去更为舒适.

@Configuration
public class ShiroConfig {

    ......

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // 自定义过滤器
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("myFilter", new MyFilter());
        shiroFilterFactoryBean.setFilters(filters);

        // 定义过滤链
        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

        return shiroFilterFactoryBean;
    }

    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();

        // 不需要拦截的访问
        // 对静态资源设置匿名访问
        chainDefinition.addPathDefinition("/index.jsp", "anon");
        chainDefinition.addPathDefinition("/login.jsp", "anon");
        chainDefinition.addPathDefinition("/favicon.ico**", "anon");
        chainDefinition.addPathDefinition("/captcha/captchaImage**", "anon");
        chainDefinition.addPathDefinition("/static/**","anon");

        // 登录,
        chainDefinition.addPathDefinition("/login", "anon");
        // 注册
        chainDefinition.addPathDefinition("/register", "anon");
        // 错误页面
        chainDefinition.addPathDefinition("/error","anon");
        // 登出,shiro 自动清除 session
        chainDefinition.addPathDefinition("/logout","logout");
        // druid连接池的角色控制,只有拥有admin角色的admin用户可以访问,不理解可以先不管
        chainDefinition.addPathDefinition("/druid/**","authc, roles[admin]");

        // 其余资源都交给 MyFilter 这个过滤器处理
        chainDefinition.addPathDefinition("/**","myFilter");
        return chainDefinition;
    }
}

DefaultShiroFilterChainDefinition内部使用 LinkedHashMap 实现 . HashMap 是无序的,LinkedHashMap 将会按序加载。最后添加了 chainDefinition.addPathDefinition(“/**”, “authc”), 如果使用 HashMap 将会优先加载了此配置,导致其他配置失效。

常见过滤器

配置缩写对应的过滤器功能
anonAnonymousFilter指定 url 可以匿名访问
authcFormAuthenticationFilter基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure)

AuthorizationInfo 授权

shiro 对权限授权划分为:角色 和 资源

  • 角色

  • 资源权限
    ————————————————
    注解方式授权

  • @RequiresAuthentication: 使用该注解标注的类,实例,方法在访问或调用时,当前 Subject 必须在当前 session 中已经过认证。

  • @RequiresGuest: 使用该注解标注的类,实例,方法在访问或调用时,当前 Subject 可以是“gust”身份,不需要经过认证或者在原先的 session 中存在记录。

  • @RequiresPermissions: 当前 Subject 需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前 Subject 不具有这样的权限,则方法不会被执行。

  • @RequiresRoles: 当前 Subject 必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天 Subject 不同时拥有所有指定角色,则方法不会执行还会抛出 AuthorizationException 异常。

  • @RequiresUser:当前 Subject 必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。

    实例

issuses

  1. ShiroFilterFactoryBean 方法名取名为 shiroFilterFactoryBean()
  2. logout功能。如果再拦截器链中配置了 logout,那么不要再定义 controller。只定义其中一个
@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();

        chainDefinition.addPathDefinition("/logout","logout");

        return chainDefinition;
    }
}

====================

@RestController
public class UserController {
    @GetMapping("/logout")
    public void logout(){
        SecurityUtils.getSubject().logout();
        System.out.println("登出");
    }
}

3.shiro 中出现 does not support authentication token

/**
    * 大坑!,必须重写此方法,不然Shiro会报错
    */
@Override
public boolean supports(AuthenticationToken token) {
    return token instanceof JwtToken;
}
  1. Filter 中的依赖注入.在配置的过程中, 一个坑点就是: 如果你希望将你的 JWTUtil 工具类(或许其它)通过依赖注入的方式注入到你的自定义 filter 中, 绝对不能使用 Autowired 注解, 因为 filter 的初始化早于 beans 的初始化, 因此是无法将 bean 通过 autowired 注入到 filter 类中的. 解决方法是: 通过为 filter 类增添构造函数, 在构造函数中传入 ApplicationContext, 然后在通过 context 获取 bean.
public class Myfilter{

    private JwtUtils jwtUtils;
    public LoginFilter(ApplicationContext context) {
        this.util = context.getBean(MyJWTUtil.class);
    }

}

changelog

  • 2021 年发布 shiro1.8 带来了质的飞跃,对于本文的需求来说,最利好的包括两点
  • 一是增加了对 SpringBoot 自动装配机制的支持;
  • 二是增加了 BearerHttpAuthenticationFilter 这个默认过滤器,从而让 Jwt 的整合获得了原生级的适配性。以上两项特性大大精简了我们的配置工作,且让当前网络上所有的教程都落后于时代。(包括官网和英文网络,搜到的教程基本都是旧版本的配置。)

阅读资料

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Shiro是一个Java安全框架,可以提供身份验证、授权、加密和会话管理等功能,Spring Boot是一个快速开发框架,可以帮助开发人员更快地构建和部署应用程序,JWT(JSON Web Token)是一种轻量级的身份验证和授权机制。将这个框架结合起来,可以构建一个安全的Web应用程序。 以下是一个简单的Shiro+Spring Boot+JWT项目的实现步骤: 1.创建一个Spring Boot项目,并添加ShiroJWT依赖项: ``` <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.7.1</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.10.7</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.10.7</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.10.7</version> <scope>runtime</scope> </dependency> ``` 2.创建一个Shiro配置类,配置Shiro的安全策略和过滤器链: ``` @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/**", "jwt"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public DefaultWebSecurityManager securityManager(Realm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); return securityManager; } @Bean public Realm realm() { return new UserRealm(); } @Bean public JwtFilter jwtFilter() { return new JwtFilter(); } } ``` 3.创建一个自定义Realm类,实现Shiro的认证和授权逻辑: ``` public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); User user = (User) principals.getPrimaryPrincipal(); authorizationInfo.addRole(user.getRole()); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); User user = userService.findByUsername(username); if (user == null) { throw new UnknownAccountException(); } return new SimpleAuthenticationInfo(user, user.getPassword(), getName()); } } ``` 4.创建一个JwtFilter类,实现JWT的认证逻辑: ``` public class JwtFilter extends AuthenticatingFilter { @Autowired private UserService userService; @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader("Authorization"); if (StringUtils.isEmpty(token)) { throw new UnauthorizedException(); } JwtToken jwtToken = new JwtToken(token); try { getSubject(request, response).login(jwtToken); } catch (AuthenticationException e) { throw new UnauthorizedException(); } return true; } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return false; } } ``` 5.创建一个JwtToken类,实现JWT的Token逻辑: ``` public class JwtToken implements AuthenticationToken { private String token; public JwtToken(String token) { this.token = token; } @Override public Object getPrincipal() { return JwtUtils.getSubject(token); } @Override public Object getCredentials() { return token; } } ``` 6.创建一个UserController类,实现用户登录和获取用户信息的逻辑: ``` @RestController public class UserController { @Autowired private UserService userService; @PostMapping("/login") public Result login(@RequestBody User user) { String token = userService.login(user); return Result.success(token); } @GetMapping("/user") public Result getUserInfo() { User user = (User) SecurityUtils.getSubject().getPrincipal(); return Result.success(user); } } ``` 7.创建一个UserService类,实现用户登录和生成JWT Token的逻辑: ``` @Service public class UserService { @Autowired private UserMapper userMapper; public User findByUsername(String username) { return userMapper.findByUsername(username); } public String login(User user) { User realUser = findByUsername(user.getUsername()); if (realUser == null || !realUser.getPassword().equals(user.getPassword())) { throw new UnauthorizedException(); } return JwtUtils.generateToken(realUser.getId(), realUser.getUsername(), realUser.getRole()); } } ``` 8.创建一个JwtUtils类,实现JWT的Token生成和解析逻辑: ``` public class JwtUtils { private static final String SECRET = "secret"; private static final long EXPIRATION_TIME = 86400000; // 24 hours public static String generateToken(String id, String username, String role) { Date now = new Date(); Date expirationDate = new Date(now.getTime() + EXPIRATION_TIME); return Jwts.builder() .setId(id) .setSubject(username) .claim("role", role) .setIssuedAt(now) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, SECRET) .compact(); } public static String getSubject(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody() .getSubject(); } } ``` 这样,就可以使用Shiro+Spring Boot+JWT构建一个安全的Web应用程序了。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值