美文网首页Java
Shiro初体验

Shiro初体验

作者: 愤怒的老照 | 来源:发表于2020-03-06 11:15 被阅读0次

简介:

Apache Shiro 是 Java 的一个安全(权限)框架。
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。
Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存 等。

Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。这不就是我们想要的嘛,而且Shiro的API也是非常简单;其基本功能点如下图所示:

[图片上传失败...(image-fb88c2-1583464531431)]

Authentication:身份认证/登录,验证用户是不是拥有相应的身份,例如账号密码登陆;

Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Web Support:Web支持,可以非常容易的集成到Web环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing:提供测试支持;

Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。

shiro自带的拦截器

默认拦截器名 拦截器类 说明(括号里的表示默认值)
认证相关:
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter 基于表单的拦截器;如 “/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure);
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter Basic HTTP 身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application);
logout org.apache.shiro.web.filter.authc.LogoutFilter 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/); 示例 “/logout=logout”
anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例 “/static/**=anon”
授权相关:
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例 “/admin/**=roles[admin]”
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter 权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样;示例 “/user/**=perms["user:create"]”
其他:
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter 不创建会话拦截器,调用 subject.getSession(false) 不会有什么问题,但是如果 subject.getSession(true) 将抛出 DisabledSessionException 异常;

shiro认证流程

  • subject(主体)请求认证,调用subject.login(token)
  • SecurityManager (安全管理器)执行认证
  • SecurityManager通过ModularRealmAuthenticator进行认证。
  • ModularRealmAuthenticator将token传给realm,realm根据token中用户信息从数据库查询用户信息(包括身份和凭证)
  • realm如果查询不到用户给ModularRealmAuthenticator返回null,ModularRealmAuthenticator抛出异常(用户不存在)
  • realm如果查询到用户给ModularRealmAuthenticator返回AuthenticationInfo(认证信息)
  • ModularRealmAuthenticator拿着AuthenticationInfo(认证信息)去进行凭证(密码)比对。如果一致则认证通过,如果不致抛出异常(凭证错误)。

shiro授权流程

  • 对subject进行授权,调用方法isPermitted("")或者hasRole("")
  • SecurityManager执行授权,通过ModularRealmAuthorizer执行授权
  • ModularRealmAuthorizer执行realm(自定义的CustomRealm)从数据库查询权限数据调用realm的授权方法:doGetAuthorizationInfo
  • realm从数据库查询权限数据,返回ModularRealmAuthorizer
  • ModularRealmAuthorizer调用PermissionResolver进行权限串比对
  • 如果比对后,isPermitted中"permission串"在realm查询到权限数据中,说明用户访问permission串有权限,否则没有权限,抛出异常。

实际应用

最近在项目中使用到shiro了,是用jwt做无状态登录,Redis做授权的缓存。按步骤记录一下

  • 首先应该明白的是,shiro的默认配置是通过cookie和自带的session来实现缓存和Remember Me这些功能,如果要做无状态应用,应该禁止shiro的session功能。

  • 整个项目分为账号密码登录和jwt登录两种方式,所以需要至少两种Realm,多Realm的认证策略有以下三种

    [1]AtLeastOneSuccessfulStrategy
    如果一个(或更多)验证成功,则整体的尝试被认为是成功的。如果没有一个验证成功,则整体失败。说白了就是,至少有一个Realm的验证是成功的算才认证通过,否则认证失败。

    [2]FirstSuccessfulStrategy
    第一个Realm成功验证返回的信息将被使用,其他的Realm将被忽略。如果没有一个Realm验证成功,则整体失败,和第一个的区别就在于,AtLeastOneSuccessfulStrategy是将所有的认证信息都返回,而FirstSuccessfulStrategy认定一个成功则返回。

    [3]AllSuccessfulStrategy
    所有配置的Realm都必须验证成功才算认证通过,否则认证失败。
    在这里使用AtLeastOneSuccessfulStrategy比较好。

  • 使用ModularRealmAuthenticator后,如果出现异常,shiro并没有抛出具体异常,而是捕获后重新抛出整体没有认证成功的异常,如果想要抛出具体的异常,需要继承ModularRealmAuthenticator,重写doMultiRealmAuthentication方法将异常抛出。

  • 首先应该做登录的realm,使用数据库校验就可以判断登录的用户名密码是否正确。

  • 其次是jwt登录校验,shiro并没有有关jwt的拦截器和校验器,都需要自己重写。

  • 全部配置完成后,理应对授权做缓存,使用Redis做缓存。

代码

ShiroConfig:

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, UserService userService){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("tokenFilter",createAuthFilter(userService));
        shiroFilterFactoryBean.setFilters(filterMap);

        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        map.put("/login", "noSessionCreation,anon");
        // 先测试,后面再换
        //map.put("/insuranceType/list", "noSessionCreation,tokenFilter,roles[member]");
        map.put("/**", "noSessionCreation,anon");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        return shiroFilterFactoryBean;
    }


    @Bean
    protected SessionStorageEvaluator sessionStorageEvaluator(){
        DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        return sessionStorageEvaluator;
    }

    @Bean
    public DefaultWebSecurityManager getSecurityManager(JwtShiroRealm jwtRealm, DbShiroRealm dbShiroRealm, CacheManager cacheManager){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealms(Arrays.asList(jwtRealm, dbShiroRealm));
        defaultWebSecurityManager.setCacheManager(cacheManager);

        //扩展父类原方法,捕获原始异常
        MultiRealmAuthenticator authenticator = new MultiRealmAuthenticator();
        //设置两个Realm,一个用于用户登录验证和访问权限获取;一个用于jwt token的认证
        authenticator.setRealms(Arrays.asList(jwtRealm, dbShiroRealm));
        //设置多个realm认证策略,一个成功即跳过其它的
        authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        defaultWebSecurityManager.setAuthenticator( authenticator );

        return defaultWebSecurityManager;
    }

    @Bean("cacheManager")
    public CacheManager cacheManager(RedisTemplate redisTemplate){
        CacheManager cacheManager = new RedisCacheManager();
        ((RedisCacheManager) cacheManager).setCache(new RedisCache(redisTemplate));

        return cacheManager;
    }

    //注意不要加@Bean注解
    protected JwtAuthFilter createAuthFilter(UserService userService){
        return new JwtAuthFilter(userService);
    }

}

MultiRealmAuthenticator:

@Slf4j
public class MultiRealmAuthenticator extends ModularRealmAuthenticator {
    protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
        AuthenticationStrategy strategy = this.getAuthenticationStrategy();
        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
        if (log.isTraceEnabled()) {
            log.trace("Iterating through {} realms for PAM authentication", realms.size());
        }
        Iterator var5 = realms.iterator();
        AuthenticationException authenticationException = null;
        while(var5.hasNext()) {
            Realm realm = (Realm)var5.next();
            aggregate = strategy.beforeAttempt(realm, token, aggregate);
            if (realm.supports(token)) {
                log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
                AuthenticationInfo info = null;
                Throwable t = null;
                try {
                    info = realm.getAuthenticationInfo(token);
                } catch (Throwable var11) {
                    t = var11;
                    authenticationException = (AuthenticationException)var11;
                    if (log.isDebugEnabled()) {
                        String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
                        log.debug(msg, var11);
                    }
                }
                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
                //增加此逻辑,只有authenticationException不为null,则表示有Realm较验到了异常,则立即中断后续Realm验证直接外抛
                if (authenticationException != null){
                    throw authenticationException;
                }
            } else {
                log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
            }
        }

        aggregate = strategy.afterAllAttempts(token, aggregate);
        return aggregate;
    }
}

DbRealm:

@Component
public class DbShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;

    public DbShiroRealm() {

        this.setCredentialsMatcher((AuthenticationToken token, AuthenticationInfo info) -> {
            UsernamePasswordToken userToken = (UsernamePasswordToken) token;
            //要验证的明文密码
            String plaintext = new String(userToken.getPassword());
            //数据库中的加密后的密文
            String hashed = info.getCredentials().toString();

            return BCrypt.checkpw(plaintext, hashed);
        });
        this.setCachingEnabled(true);
        this.setAuthorizationCachingEnabled(true);
    }

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken userpasswordToken = (UsernamePasswordToken)token;
        String username = userpasswordToken.getUsername();

        User checkUser = new User();
        checkUser.setState(UserState.NORMAL.getState());
        // 通过Email登录
        if (Validator.isEmail(username)){
            checkUser.setEmail(username);
        }else if(Validator.isIdCard(username)){
            // 通过身份证登录
            checkUser.setIdCard(username);
        }else if(Validator.isPhone(username)){
            //通过手机号登录
            checkUser.setPhone(username);
        }else {
            // 通过用户名登录;修改用户名时必须做限定
            checkUser.setUsername(username);
        }

        User user = userService.getUserByRules(checkUser);
        if(user == null)
            throw new AuthenticationException("用户名或者密码错误");
        return new SimpleAuthenticationInfo(user, user.getPassword(),getName());
    }


    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) principals.getPrimaryPrincipal();
        user.setRoles(roleService.findRolesByUserId(user.getId()));
        Set<Role> roles = user.getRoles();
        List<String> roleNames = roles.stream().map(Role::getName).collect(Collectors.toList());
        simpleAuthorizationInfo.addRoles(roleNames);

        return simpleAuthorizationInfo;
    }

}

jwtRealm:

@Component
public class JwtShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    private JwtUtil jwtUtil;

    @Autowired
    public JwtShiroRealm(JwtUtil jwtUtil){
        this.jwtUtil = jwtUtil;
        this.setCachingEnabled(true);
        this.setAuthorizationCachingEnabled(true);
        this.setCredentialsMatcher(new JwtCredentialsMatcher(jwtUtil));
    }


    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
        JwtToken jwtToken = (JwtToken) authcToken;
        String token = jwtToken.getToken();
        Optional.ofNullable(token).orElseThrow(()-> new AuthenticationException(ResponseCode.NEED_LOGIN.getDesc()));

        Map<String, String> map;
        // 校验
        try{
            map = jwtUtil.verifyToken(token);
        }catch (TokenExpiredException e){
            throw new AuthenticationException(ResponseCode.TOKEN_EXPIRES.getDesc());
        }catch (Exception e){
            throw new AuthenticationException("token无效,请重新登陆");
        }

        String host = map.get("host");
        if (host == null || !host.equals(jwtToken.getHost())){
            throw new AuthenticationException("token地区错误,请重新登录");
        }


        int id = Integer.parseInt(map.get("id"));
        User user = userService.getUserById(id);

        if(user == null)
            throw new AuthenticationException("token过期,请重新登录");
        return new SimpleAuthenticationInfo(user, null, getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("进行数据库权限读取");

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) principals.getPrimaryPrincipal();
        user.setRoles(roleService.findRolesByUserId(user.getId()));
        Set<Role> roles = user.getRoles();
        List<String> roleNames = roles.stream().map(Role::getName).collect(Collectors.toList());
        simpleAuthorizationInfo.addRoles(roleNames);

        return simpleAuthorizationInfo;
    }
}

jwtFilter:


@Slf4j
public class JwtAuthFilter extends AuthenticatingFilter {
    private static final int tokenRefreshInterval = 300;
    private String tokenError = "";
    private UserService userService;

    public JwtAuthFilter(UserService userService){
        this.userService = userService;
        this.setLoginUrl("/login");
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) //对于OPTION请求做拦截,不做token校验
            return false;

        return super.preHandle(request, response);
    }

    @Override
    protected void postHandle(ServletRequest request, ServletResponse response){
        this.fillCorsHeader(WebUtils.toHttp(request), WebUtils.toHttp(response));
        request.setAttribute("jwtShiroFilter.FILTERED", true);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if(this.isLoginRequest(request, response))
            return true;
        Boolean afterFiltered = (Boolean)(request.getAttribute("jwtShiroFilter.FILTERED"));
        if( BooleanUtils.isTrue(afterFiltered))
            return true;

        boolean allowed = false;
        try {
            allowed = executeLogin(request, response);
        } catch(IllegalStateException e){ //not found any token
            e.printStackTrace();
            log.error("Not found any token");
        }catch (Exception e) {
            log.error("Error occurs when login", e);
        }
        return allowed || super.isPermissive(mappedValue);
    }


    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
        HttpServletRequest request = null;
        Optional<String> result = null;
        String host = null;
        if (servletRequest instanceof HttpServletRequest){
            request = (HttpServletRequest)servletRequest;
            String token = request.getHeader("authorization");

            result = Optional.ofNullable(token)
                    .filter(item -> item.startsWith("bearer"))
                    .map(item -> item.substring(6));

            host = WebUtil.getRealRemoteAddr(request);

        }else {
            result = Optional.empty();
        }

        return new JwtToken(result.orElse(null), host);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletResponse httpResponse = WebUtils.toHttp(servletResponse);
        httpResponse.setCharacterEncoding("UTF-8");
        httpResponse.setContentType("application/json;charset=UTF-8");
        httpResponse.setStatus(HttpStatus.SC_LENGTH_REQUIRED);

        int errorCode = 0;
        String errorMsg = "";
        try {
            if (ResponseCode.TOKEN_EXPIRES.getDesc().equals(servletRequest.getAttribute(this.tokenError))){
                errorCode = ResponseCode.TOKEN_EXPIRES.getCode();
                errorMsg = ResponseCode.TOKEN_EXPIRES.getDesc();
            }else {
                errorCode = ResponseCode.ERROR.getCode();
                errorMsg = (String)servletRequest.getAttribute(this.tokenError);
            }
        } catch (Exception e1) {
            errorCode = ResponseCode.ERROR.getCode();
            errorMsg = ResponseCode.ERROR.getDesc();
            e1.printStackTrace();
        }

        fillCorsHeader(WebUtils.toHttp(servletRequest), httpResponse);
        ResponseWriter.println(httpResponse, ServerResponse.createByErrorCodeMessage(errorCode, errorMsg));
        return false;
    }


    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        request.setAttribute(this.tokenError, e.getMessage());
        log.error("Validate  fail, token:{}, error:{}", token.toString(), e.getMessage());
        return false;
    }

    protected void fillCorsHeader(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse){
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
    }
}

JwtCredentialsMatcher:

@NoArgsConstructor
public class JwtCredentialsMatcher implements CredentialsMatcher {
    private JwtUtil jwtUtil;
    public JwtCredentialsMatcher(JwtUtil jwtUtil){
        this.jwtUtil = jwtUtil;
    }

    @Override
    public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
        JwtToken jwtToken = (JwtToken) authenticationToken;

        String token = jwtToken.getToken();
        // 校验
        try{
            jwtUtil.verifyToken(token);
        }catch (Exception e){
            return false;
        }

        return true;
    }
}

RedisCacheManager:

public class RedisCacheManager implements CacheManager {

    private Cache cache;

    public void setCache(Cache cache) {
        this.cache = cache;
    }

    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return cache;
    }
}

RedisCache:

@Data
@NoArgsConstructor
@Slf4j
public class RedisCache<K,V> implements Cache<K,V> {

    private RedisTemplate redisTemplate;

    // 过期时间,单位为分钟
    private static final Integer expire_time = 30;
    // 缓存前缀
    private static final String cache_prefix = "shiro-cache:";

    public RedisCache(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }

    private String getKey(K k){
        if (k instanceof String){
            return cache_prefix + k;
        }else if(k instanceof SimplePrincipalCollection){
            return cache_prefix + ((User)((SimplePrincipalCollection)k).getPrimaryPrincipal()).getUsername();
        }

        return cache_prefix + k.toString();
    }
    @Override
    public V get(K key) throws CacheException {
        V v = (V)redisTemplate.opsForValue().get(getKey(key));
        log.info("从redis中取出权限对象[{}]" , v);
        return v;
    }

    @Override
    public V put(K k, V v) throws CacheException {
        redisTemplate.opsForValue().set(getKey(k), v, expire_time, TimeUnit.SECONDS);
        log.info("[k={},v={}]将权限存放到Redis中", k, v);
        return v;
    }

    @Override
    public V remove(K k) throws CacheException {
        return null;
    }

    @Override
    public void clear() throws CacheException {

    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public Set<K> keys() {
        return null;
    }

    @Override
    public Collection<V> values() {
        return null;
    }
}

相关文章

  • Shiro初体验

    简介: Apache Shiro 是 Java 的一个安全(权限)框架。Shiro 可以非常容易的开发出足够好的...

  • JavaWeb日记——Shiro初体验

    用户登录功能大部分项目都会有,但如果要多种不同身份不同权限就比较难实现,Shiro正好是为了解决这个问题而诞生的 ...

  • shiro初体验,蹒跚学步

    1-登录登出 shiro.ini ShiroTest 异常解析 2-自定义realm 继承AuthorizingR...

  • 权限管理框架--Shiro

    1.shiro概述 1.1shiro是什么 1.2shiro能做什么 1.3Shiro特性 1.4shiro架构 ...

  • SpringBoot+Shiro+JWT

    SpringBoot+Shiro+JWT 一、Shiro 1、什么是shiro? Apache Shiro是一个强...

  • Java框架--Shiro入门

    目录 shiro 是什么? shiro 可以解决什么问题? shiro 名词解释&认证流程 shiro 快速入门 ...

  • Shiro入门

    Shiro 什么是Shiro? Apache Shiro 是一个Java的安全框架。 关于shiro的相关概念: ...

  • springboot + shiro +cas 集成

    shiro 认证流程图: shiro 授权流程图: shiro 认证时序图: shiro + cas 认证时序图:...

  • Shiro安全框架

    Shiro Session管理Shiro 缓存管理Shiro集成SpringShiro集成SpringBoot:h...

  • Apache Shiro 是什么 & Shiro 框架介绍

    Apache Shiro 是什么 & Shiro 框架介绍 官方介绍 Apache Shiro™ is a pow...

网友评论

    本文标题:Shiro初体验

    本文链接:https://www.haomeiwen.com/subject/drgvrhtx.html