【AgileTC】_Shiro权限管理

【AgileTC】_Shiro权限管理

  • 1.在pom.xml文件中导入shiro相关依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.5.3</version>
    </dependency>
  • 2.编写shiro配置文件(xml文件或者@Configuration注解配置类)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    @Configuration
    public class ShiroConfig {

    /**
    * 配置web容器-请求过滤器
    * /api为拦截路径前缀
    * 拦截相关的内容请看 {@code AuthFilter.class}
    */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
    Map<String, Filter> filterMap = new LinkedHashMap<>();
    filterMap.put("header", new AuthFilter());

    Map<String, String> ruleMap = new LinkedHashMap<>();
    ruleMap.put("/api/**", "header");

    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    bean.setSecurityManager(securityManager);
    bean.setUnauthorizedUrl("/403");
    bean.setFilters(filterMap);
    bean.setFilterChainDefinitionMap(ruleMap);
    return bean;
    }

    /**
    * {@code RequiresPermissions.class}
    * {@code RequiresRoles.class}
    * 权限注解的解析+装饰器,装配到容器中
    */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
    return authorizationAttributeSourceAdvisor;
    }

    /**
    * 配置安全管理器,同时将Realm塞入进来
    */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(Realm realm, CacheManager cacheManager) {
    DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
    manager.setRealm(realm);
    manager.setCacheManager(cacheManager);
    return manager;
    }

    /**
    * 用于支持shiro的注解生效
    * @see AuthorizationAttributeSourceAdvisor
    * 不注册这个会导致request不生效,走不到controller
    */
    @Bean
    public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
    creator.setProxyTargetClass(true);
    return creator;
    }

    }
  • 3.创建一个继承AuthorizingRealm的Realm类,重写设置缓存方法、认证方法和授权方法:

    • 设置缓存方法:开启缓存并为缓存命名;
    • 认证方法:从数据库中根据token查询用户信息,生成用户认证信息;
    • 授权方法:从数据库中根据用户信息查询用户关联的角色和权限,生成用户授权信息;
    • 查看shiro框架源码,可知上述三个方法之间的工作关系:shiro过滤器拦截到相应请求时,先根据token从认证缓存中查找用户认证信息,没找到则执行此处认证方法,并将用户认证信息放入缓存中;然后根据用户信息从授权缓存中查找用户授权信息,没找到则执行此处授权方法,并将用户授权信息放入缓存中。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    @Component(value = "realm")
    public class AgileRealm extends AuthorizingRealm implements Serializable {

    private static final long serialVersionUID = -2741710248922440453L;

    private static final Logger LOGGER = LoggerFactory.getLogger(AgileRealm.class);

    private static final String REALM_NAME = "AGILE-REALM";

    @Override
    public void setCacheManager(@Autowired CacheManager cacheManager) {
    super.setCacheManager(cacheManager);

    //实际上此处两个缓存不命名,shiro也会为其自动命名;我们为其命名的原因主要是后续需要获取到这两个缓存,在数据更新是清除对应缓存,之所以要这么做,是因为shiro并没有提供缓存更新接口。
    //认证缓存开启
    super.setAuthenticationCachingEnabled(true);
    //为认证缓存命名
    super.setAuthenticationCacheName("AGILE-REALM-AuthenticationCache");
    //授权缓存开启
    super.setAuthorizationCachingEnabled(true);
    //为授权缓存命名
    super.setAuthorizationCacheName("AGILE-REALM-AuthorizationCache");
    }

    @Resource
    private UserMapper userMapper;

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

    /**
    * 认证操作
    *
    * @param token 用户登录后的身份信息
    * @return 认证后的基础信息
    */
    @Override
    @Transactional(rollbackFor = Exception.class)
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    LOGGER.info("[Login]Username:{}, timestamp:{}", token.toString(), LocalDateTime.now().format(DateTimeFormatter.ISO_DATE));

    AgileToken principal = (AgileToken) token.getPrincipal();

    // 从数据库查信息,导出用户信息
    User user = userMapper.getUser(principal.getUsername(), principal.getChannel(), principal.getLineId());

    // 角色相关的校验
    if (user == null) {

    //如果数据库中没有这个用户,则报错。
    throw new CaseServerException("该用户不存在",StatusCode.AUTH_UNKNOWN);

    } else if (AuthConstant.BLOCKED.equals(user.getIsBlock())) {
    throw new CaseServerException(StatusCode.AUTH_BLOCKED);
    }

    // 给用户设置默认的信息,由于我们不需要管理用户登录态,所以只需要塞入即可
    // 就是表示该用户认证通过,返回认证后取得认证令牌的用户。
    return new SimpleAuthenticationInfo(principal, principal, REALM_NAME);
    }

    /**
    * 授权操作
    *
    * @param principalCollection 认证后的信息集合
    * @return 授权信息
    */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    AgileToken principal = (AgileToken) principalCollection.getPrimaryPrincipal();

    // 从数据库查询信息,导出权限相关的信息
    // 就是从数据库中通过用户、角色、权限三个表查出该用户对应的权限信息
    User user = userMapper.getUserWithPerms(principal.getUsername(), principal.getChannel(), principal.getLineId());

    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

    if (user == null) {
    throw new CaseServerException("您没有权限进行此操作", StatusCode.AUTH_ERROR);
    } else if (CollectionUtils.isEmpty(user.getPermissions())) {
    throw new CaseServerException("你没有权限进行此操作", StatusCode.AUTH_ERROR);
    } else {
    List<String> perms = user.getPermissions().stream().map(Permission::getResource).collect(Collectors.toList());

    //为该用户进行权限授权,将perms列表的权限授权给该用户;
    //因为本项目没有使用角色校验,所以不用为用户进行角色授权。
    info.addStringPermissions(perms);
    }

    return info;
    }
    }
  • 4.创建一个实现AuthenticationToken接口的token类,用于保存用户信息,具体包括哪些信息可以由我们自己设定。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @Data
    public class AgileToken implements AuthenticationToken {

    private static final long serialVersionUID = 5288207035894094853L;

    private String username;

    private Integer channel;

    private Long lineId;

    public AgileToken(String username, String channel, String lineId) {
    this.username = username;
    this.channel = Integer.parseInt(channel);
    this.lineId = Long.parseLong(lineId);
    }

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

    @Override
    public Object getCredentials() {
    return this;
    }
    }
  • 5.创建一个实现Cache<K, V>的缓存类,内部版本是使用Redis客户端Jedis来作为缓存实现结构的,但是开源版本设计成单机项目,此处使用HashMap来作为缓存的实现结构。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    public class AgileCache<K, V> implements Cache<K, V> {

    private static final Logger LOGGER = LoggerFactory.getLogger(AgileCache.class);

    private HashMap cacheMap;

    public AgileCache(HashMap cacheMap) {
    this.cacheMap = cacheMap;
    }

    @Override
    public V get(K k) throws CacheException {
    if (LOGGER.isInfoEnabled()) {
    LOGGER.info("[Shiro Get]{}", k);
    }

    if (k == null) {
    return null;
    }

    Object res = cacheMap.get(k);
    if (res == null) {
    return null;
    }

    return (V) res;
    }

    @Override
    public V put(K k, V v) throws CacheException {
    if (k == null || v == null) {
    LOGGER.error("[Shiro Put]key={}, value={}", k, v);
    throw new CaseServerException("不能塞入空值", StatusCode.SERVER_BUSY_ERROR);
    }
    if (LOGGER.isInfoEnabled()) {
    LOGGER.info("[Shiro Put]key={}, value={}", k, v);
    }
    cacheMap.put(k, v);
    return v;
    }

    @Override
    public V remove(K k) throws CacheException {
    if (k == null) {
    return null;
    }
    V res = get(k);
    cacheMap.remove(k);
    return res;
    }

    @Override
    public void clear() throws CacheException {
    // should do nothing
    }

    @Override
    public int size() {
    // should do nothing
    return 0;
    }

    @Override
    public Set<K> keys() {
    // should do nothing
    return new HashSet<>();
    }

    @Override
    public Collection<V> values() {
    // should do nothing
    return new ArrayList<>();
    }
    }
  • 6.创建一个继承AbstractCacheManager的缓存控制器,shiro配置文件会将其配置到spring容器中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component
    public class AgileCacheManager extends AbstractCacheManager {

    private HashMap cacheMap = new HashMap();

    @Override
    protected Cache createCache(String s) throws CacheException {
    return new AgileCache(cacheMap);
    }
    }
  • 7.创建一个继承BasicHttpAuthenticationFilter的shiro过滤器,该过滤器主要进行一些前处理,此处用于将请求头中的用户名取出创建一个token,用户shiro后续认证和授权;查看shiro源码可知该过滤器中isAccessAllowed()方法被一个prehandle方法调用,executeChain()被一个doFilter方法调用,所有前者先执行,后者后执行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    public class AuthFilter extends BasicHttpAuthenticationFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(AuthFilter.class);

    private static final String LOGIN_SIGN = "username";

    /**
    * 判断请求头中有没有用户名请求头,有则说明该请求需要进行登录校验。
    */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
    HttpServletRequest req = (HttpServletRequest) request;
    return StringUtils.hasText(req.getHeader(LOGIN_SIGN));
    }

    /**
    * 登录校验
    */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    HttpServletRequest req = (HttpServletRequest) request;

    String username = req.getHeader(LOGIN_SIGN);

    // 直接使用用户名来做token
    AgileToken token = new AgileToken(username, AuthConstant.DEFAULTCHANNEL,AuthConstant.DEFAULTLINEID);

    // 获取用户实例
    Subject subject = getSubject(request, response);
    // 登录
    subject.login(token);
    return true;
    }

    /**
    * 校验用户是否正确登录
    */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    if (isLoginAttempt(request, response)) {
    try {
    executeLogin(request, response);
    } catch (Exception e) {
    LOGGER.error("登录出错,错误原因={}", e.getLocalizedMessage());
    e.printStackTrace();
    throw new CaseServerException(StatusCode.AUTH_UNKNOWN);
    }
    }
    return true;
    }

    /**
    * 先对请求进行数据封装,然后交给后续过滤器。
    */
    @Override
    protected void executeChain(ServletRequest request, ServletResponse response, FilterChain chain) throws Exception {
    ShiroHttpServletRequest req = (ShiroHttpServletRequest)request;
    HttpServletResponse resp = (HttpServletResponse) response;

    chain.doFilter(req, resp);
    }
    }
  • 8.创建更新缓存中数据的ShiroHelper类,在用户角色或权限被修改时,删除缓存中的用户信息键值对:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public class ShiroRedisHelper {

    private static AgileCacheManager cacheManager = SpringUtils.getBean(AgileCacheManager.class);

    private static Logger LOGGER = LoggerFactory.getLogger(ShiroRedisHelper.class);

    /**
    * 清除指定用户的认证缓存和授权缓存
    */
    public static void delete(AgileToken token) {
    PrincipalCollection principals = new SimpleAuthenticationInfo(token, token, "AGILE-REALM").getPrincipals();
    deleteAuthorizationInfo(principals);
    deleteAuthenticationInfo(token);
    }

    /**
    * 清除指定用户的授权缓存
    */
    public static void deleteAuthorizationInfo(PrincipalCollection principals) {
    Cache<Object, AuthorizationInfo> cache = cacheManager.getCache("AGILE-REALM-AuthorizationCache");

    LOGGER.info("[Authorization_redis_delete]key:{}, timestamp:{}", principals.toString(), LocalDateTime.now().format(DateTimeFormatter.ISO_DATE));

    cache.remove(principals);
    }

    /**
    * 清除指定用户的认证缓存
    */
    public static void deleteAuthenticationInfo(AgileToken principal) {
    Cache<Object, AuthenticationInfo> cache = cacheManager.getCache("AGILE-REALM-AuthenticationCache");

    LOGGER.info("[Authentication_redis_delete]key:{}, timestamp:{}", principal.toString(), LocalDateTime.now().format(DateTimeFormatter.ISO_DATE));

    cache.remove(principal);
    }
    }
  • 9.在相关接口上添加权限控制注解,如@RequiresPermissions:

    1
    2
    3
    4
    5
    @GetMapping(value = "/getCaseInfo")
    @RequiresPermissions("case:detail")
    public Response<?> getCaseGeneralInfo(@RequestParam @NotNull(message = "用例id为空") Long id) {
    return Response.success(caseService.getCaseGeneralInfo(id));
    }