石一歌的Shiro笔记


Shiro

概念

img

  • Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
  • SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
  • Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。

springboot整合shiro

对密码部分进行了增强,使用随机值加盐加密

pom

		
			org.apache.shiro
			shiro-spring-boot-web-starter
			1.6.0
		

LoginController

@RequestMapping("/login")
    public String login(String username, String password, Model model) {
        //获得主体
        Subject subject = SecurityUtils.getSubject();
        //获取token并进行登录及异常处理
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
            return "index";
        } catch (UnknownAccountException e) {
            model.addAttribute("msg", "用户名错误!");
            return "login";
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "密码错误!");
            return "login";
        }
    }

UserRealm

在用户域进行授权和认证

public class UserRealm extends AuthorizingRealm {
    @Resource
    UserService userService;
    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1.从主体传过来的认证信息中,获得用户名
        String username = (String) authenticationToken.getPrincipal();
        // 2.通过用户名到数据库中获取凭证
        User user = userService.getUserByUserName(userName);
        if (user == null) {
            return null;
        }
        return new SimpleAuthenticationInfo(user,//用户/用户名
                                            user.getPassword(),//数据库拿到的密码
                                         	ByteSource.Util.bytes(user.getSalt()),//salt
                                            this.getName());//RealmName
    }
    //授权(已通过认证)
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        User user = (User) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        
        for (SysRole role : user.getRoles()) {
            info.addRole(role.getName());
            for (SysPermission permission : role.getPermissions()) {
                info.addStringPermission(permission.getName());
            }
        }
        return info;
    }  
}

ShiroConfig

@Configuration
public class ShrioConfig {
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        //添加 Shiro 的内置过滤器
        /*
            anon : 无需认证,就可以访问
            authc : 必须认证,才能访问
            user : 必须拥有 "记住我"功能才能用
            perms : 拥有对某个资源的权限才能访问
            role : 拥有某个角色权限才能访问
          	有顺序问题,/**通常在最下面
         */
        Map map = new LinkedHashMap();
        //此处的过滤器可以使用shiro相应的注解实现
        map.put("/static/**", "anon");
        map.put("/logout", "logout");
        map.put("/login", "anon")
        map.put("/**", "authc");
        bean.setFilterChainDefinitionMap(map);
        //设置登录页
        bean.setLoginUrl("/toLogin");
        //设置首页
        bean.setSuccessUrl("/index");
        //设置未授权页面
        bean.setUnauthorizedUrl("/403");
        return bean;
    }

    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        //关联用户域
        return manager.setRealm(userRealm);
    }

    @Bean(name = "userRealm")
    public UserRealm userRealm(@Qualifier("credentialsMatcher")
                               HashedCredentialsMatcher hashedCredentialsMatcher) {
        UserRealm userRealm = new UserRealm();
        //关联凭证匹配器
        return userRealm.setCredentialsMatcher(hashedCredentialsMatcher);
    }
    
    @Bean(name = "credentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 散列算法
        hashedCredentialsMatcher.setHashIterations(2); // 散列次数
        return hashedCredentialsMatcher;
    }
}

权限控制注解(在controller方法上使用)

QQ截图20220102191802

springboot整合shiro+jwt

pom

		
            com.auth0
            java-jwt
            3.4.0
        

LoginController

验证账号密码无误,会返回Token,其他请求需要携带Token访问

	@PostMapping("/login")
    public ResultMap login(@RequestParam("username") String username,
                           @RequestParam("password") String password) {
        String realPassword = userMapper.getPassword(username);
        if (realPassword == null) {
            return resultMap.fail().code(401).message("用户名错误");
        } else if (!realPassword.equals(password)) {
            return resultMap.fail().code(401).message("密码错误");
        } else {
            return resultMap.success().code(200).message(JWTUtil.createToken(username));
        }
    }

JWTUtil

创建jwt工具类,方便使用,包括生成token验证token以及获取token信息方法

public class JWTUtil {
    // 过期时间 24 小时
    private static final long EXPIRE_TIME = 60 * 24 * 60 * 1000;
    // 密钥
    private static final String SECRET = "SHIRO+JWT";

    /**
     * 生成 token
     *
     * @param username 用户名
     * @return 加密的token
     */
    public static String createToken(String username) {
        try {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            // 附带username信息
            return JWT.create()
                    .withClaim("username", username)
                    //到期时间
                    .withExpiresAt(date)
                    //创建一个新的JWT,并使用给定的算法进行标记
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }

    /**
     * 校验 token 是否正确
     *
     * @param token    密钥
     * @param username 用户名
     * @return 是否正确
     */
    public static boolean verify(String token, String username) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            //在token中附带了username信息
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            //验证 token
            verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 获得token中的信息,无需secret解密也能获得
     *
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}

JWTFilter

自定义过滤器

检验请求头是否带有 token ((HttpServletRequest) request).getHeader("Token") != null

如果带有 token,执行 shiro 的 login() 方法,将 token 提交到 Realm 中进行检验;如果没有 token,说明当前状态为游客状态

如果在 token 校验的过程中出现错误,则重定向到 /unauthorized/**

public class JWTFilter extends BasicHttpAuthenticationFilter {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 如果带有 token,则对 token 进行检查,否则直接通过
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
        //判断请求的请求头是否带上 "Token"
        if (isLoginAttempt(request, response)) {
            //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                //token 错误
                responseError(response, e.getMessage());
            }
        }
        //如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
        return true;
    }

    /**
     * 判断用户是否想要登入。
     * 检测 header 里面是否包含 Token 字段
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader("Token");
        return token != null;
    }

    /**
     * 执行登陆操作
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Token");
        JWTToken jwtToken = new JWTToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 将非法请求跳转到 /unauthorized/**
     */
    private void responseError(ServletResponse response, String message) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            //设置编码,否则中文字符在重定向时会变为空字符串
            message = URLEncoder.encode(message, "UTF-8");
            httpServletResponse.sendRedirect("/unauthorized/" + message);
        } catch (IOException e) {
            logger.error(e.getMessage());
        }
    }
}

UserRealm

public class UserRealm extends AuthorizingRealm {
    @Resource
    UserService userService;
    /**
     * 重写supports方法,使之能识别JWTToken
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }
    //认证
      @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 已经用JWTToken代替了原有token,所以这里获取的凭据可以用JWTUtil解密
        String token = (String) authenticationToken.getCredentials();
        // 解密获得username,用于和数据库进行对比
        String username = JWTUtil.getUsername(token);
        if (username == null || !JWTUtil.verify(token, username)) {
            throw new AuthenticationException("token认证失败!");
        }
        String password = userService.getPassword(username);
        if (password == null) {
            throw new AuthenticationException("该用户不存在!");
        }
        return new SimpleAuthenticationInfo(username, token, this.getName());
    }
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JWTUtil.getUsername(principals.toString());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        User user = userService.getUserByUserName(userName);
        for (SysRole role : user.getRoles()) {
            info.addRole(role.getName());
            for (SysPermission permission : role.getPermissions()) {
                info.addStringPermission(permission.getName());
            }
        }
        return info;
    }
}

ShiroConfig

更改ShiroConfig中getShiroFilterFactoryBean方法的内容,使用JWTFilter

	@Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        // 添加自己的过滤器并且取名为jwt
        Map filterMap = new LinkedHashMap<>();
        //设置我们自定义的JWT过滤器
        filterMap.put("jwt", new JWTFilter());
        bean.setFilters(filterMap);
        Map map = new HashMap<>();
        // 所有请求通过我们自己的JWT Filter
        map.put("/**", "jwt");
        // 访问 /unauthorized/** 不通过JWTFilter
        map.put("/unauthorized/**", "anon");
        bean.setFilterChainDefinitionMap(map);
        bean.setSecurityManager(securityManager);
        // 设置无权限时跳转的 url;
        bean.setUnauthorizedUrl("/unauthorized/无权限");
        return bean;
    }

权限控制注解

与上一部分相同

springboot整合shiro+jwt(fastdep)

pom


  com.louislivi.fastdep
  fastdep-shiro-jwt
  1.0.7

yaml

fastdep:
  shiro-jwt:
    filter: #shiro过滤规则
      admin:
        path: /admin/**
        role: jwt # jwt为需要进行token校验
      front:
        path: /front/**/**
        role: anon # anon为无需校验
    secret: "6Dx8SIuaHXJYnpsG18SSpjPs50lZcT52" # jwt秘钥
#    expireTime: 7200000 # token有效期
#    prefix: "Bearer "  # token校验时的前缀
#    signPrefix: "Bearer " # token生成签名的前缀
#    header: "Authorization" # token校验时的header头
#    以下对应为shiro配置参数,无特殊需求无需配置
#    loginUrl: 
#    successUrl: 
#    unauthorizedUrl: 
#    filterChainDefinitions: 

具体应用参考官方文档

参考链接

  • Shiro安全框架【快速入门】就这一篇
  • 教你 Shiro 整合 SpringBoot,避开各种坑
  • 教你 Shiro + SpringBoot 整合 JWT
  • fastdep-shiro-jwt