springboot2集成shiro认证鉴权(中篇)

上一篇中,搭建了项目的基础结构,这一篇中,将简述一下shiro的集成过程。

1、添加依赖

pom.xml中添加如下shiro依赖

<properties>
    <shiro.version>1.7.1</shiro.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring-boot-starter</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>${shiro.version}</version>
    </dependency>
</dependencies>

这里面主要是shiro的依赖,同时添加了ehcache的缓存支持,除此之外,还添加了spring cache支持,这个在后面做缓存时会用到,先提前添加进去。

2、缓存配置

(1)ehcache配置文件

resources资源目录下新建ehcache.xml配置文件,文件内容如下。在文件中,定义了两个缓存用于shiro认证和鉴权的缓存,并设置了相应的过期时间和持久化策略。

<!-- ehcache配置 -->
<ehcache
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
        updateCheck="false">
    <!--缓存路径,用户目录下的base_ehcache目录-->
    <!--    <diskStore path="user.home/base_ehcache"/>-->
    <diskStore path="base_ehcache"/>

    <defaultCache maxElementsInMemory="10000"
                  eternal="false"
                  timeToIdleSeconds="600"
                  timeToLiveSeconds="600"
                  overflowToDisk="true"
                  maxElementsOnDisk="1000000"
                  diskPersistent="false"
                  diskExpiryThreadIntervalSeconds="120"
                  memoryStoreEvictionPolicy="LRU"/>

    <!--
    name:缓存文件名,同样的可以配置多个缓存
    maxElementsInMemory:内存中最多存储
    eternal:外部存储
    overflowToDisk:超出缓存到磁盘
    diskPersistent:磁盘持久化
    timeToLiveSeconds:缓存时间
    diskExpiryThreadIntervalSeconds:磁盘过期时间
    -->

    <!-- shiro 认证缓存 -->
    <cache name="shiro-authentication-cache"
           maxElementsInMemory="20000"
           eternal="false"
           overflowToDisk="true"
           diskPersistent="false"
           timeToLiveSeconds="1800"
           diskExpiryThreadIntervalSeconds="7200"/>

    <!-- shiro 授权缓存 -->
    <cache name="shiro-authorization-cache"
           maxElementsInMemory="20000"
           eternal="false"
           overflowToDisk="true"
           diskPersistent="false"
           timeToLiveSeconds="1800"
           diskExpiryThreadIntervalSeconds="7200"/>
</ehcache>

(2)缓存配置类

com.ygr.config包下新建配置类CacheConfig,内容如下。注意其中的factoryBean.setShared(true),这一行尤为重要,因为在当前使用的版本中,ehcache有一个条件,一个jvm中只允许有一个ehcache实例,所以这里需要设置为共享模式,否则后面shiro配置缓存管理器的时候,会冲突报错。@EnableCaching注解则是开启spring cache的支持,便于使用@Cacheable等注解进行缓存操作。

@EnableCaching
@Configuration
public class CacheConfig {

    @Bean
    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
        EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
        factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
        factoryBean.setShared(true);
        factoryBean.setCacheManagerName("cacheManager");
        return factoryBean;
    }
}

3、Realm配置

shiro有三大核心组件,分别是SubjectSecurityManager以及Realm,在web应用中,Subject可理解为当前用户,SecurityManager则是安全管理器,管理shiro中的组件,Realm则是为SecurityManager提供认证授权信息。

shiro已经提供了不少的Realm,如JdbcRealmSimpleAccountRealm等,但在实际使用中,还是自己定义一个比较方便。通常,一个Realm对应一种认证方式,如果需要支持多种认证方式,那就定义多个Realm即可。在本篇中,使用的是用户名密码的方式进行认证。

新建LoginRealm,代码如下

@Slf4j
@Component
public class LoginRealm extends AuthorizingRealm {

    private final UserPrincipalService userPrincipalService;

    public LoginRealm(UserPrincipalService userPrincipalService, CacheManager cacheManager) {
        this.userPrincipalService = userPrincipalService;
        this.setCachingEnabled(true);
        this.setCacheManager(cacheManager);
        this.setAuthenticationCachingEnabled(true);
        this.setAuthorizationCachingEnabled(true);
        this.setAuthenticationCacheName("shiro-authentication-cache");
        this.setAuthorizationCacheName("shiro-authorization-cache");

        // 密码比对器 SHA-256
        HashedCredentialsMatcher hashMatcher = new HashedCredentialsMatcher();
        hashMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
        hashMatcher.setStoredCredentialsHexEncoded(false);
        hashMatcher.setHashIterations(1024);
        this.setCredentialsMatcher(hashMatcher);
    }

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

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        UserPrincipalEntity entity = (UserPrincipalEntity) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(entity.getRoles());
        info.setStringPermissions(entity.getPermissions());
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String username = usernamePasswordToken.getUsername();
        if (username == null) {
            return null;
        }
        UserPrincipalEntity userPrincipal = userPrincipalService.getUserPrincipal(username);
        if (userPrincipal == null) {
            return null;
        }
        return new SimpleAuthenticationInfo(userPrincipal, userPrincipal.getPassword(),
                new ShiroByteSource(AuthConstant.SECRET_SALT), getName());
    }
    
    @Override
    protected Object getAuthenticationCacheKey(PrincipalCollection principals) {
        UserPrincipalEntity userPrincipal = (UserPrincipalEntity) principals.getPrimaryPrincipal();
        return userPrincipal.getUsername();
    }

    /**
     * SimpleByteSource未实现Serializable接口,在缓存序列化时会报错,所以使用子类代替
     */
    public static class ShiroByteSource extends SimpleByteSource implements Serializable {
        /**
         * 父类没有无参构造器,这里加一个
         */
        public ShiroByteSource() {
            super(new byte[]{});
        }

        public ShiroByteSource(byte[] bytes) {
            super(bytes);
        }

        public ShiroByteSource(char[] chars) {
            super(chars);
        }

        public ShiroByteSource(String string) {
            super(string);
        }

        public ShiroByteSource(ByteSource source) {
            super(source);
        }

        public ShiroByteSource(File file) {
            super(file);
        }

        public ShiroByteSource(InputStream stream) {
            super(stream);
        }
    }
}

简单解释一下,UserPrincipalService是自定义的一个接口,用于获取用户信息,包括用户名、密码、角色、权限等数据,接口定义及实现见后面的代码。构造器中除了UserPrincipalService之外,还注入了CacheManager,此CacheManagershiro的,不是spring cache的,在此Realm中开启了全局缓存,并设置认证缓存与授权缓存名称;同时,设置了密码比对器,与上一篇的UaUserInfoService示例代码的加密方法对应。至于ShiroByteSource这个类纯粹是为了弥补SimpleByteSource未实现序列化接口导致无法缓存的问题,除非缓存失效,否则这个问题会一直存在。

另外,重写了getAuthenticationCacheKey方法,因为登录时使用的AuthenticationTokenUsernamePasswordToken,登录后将认证信息缓存的时候,调用的是如下的方法来获取缓存的key,而UsernamePasswordTokengetPrincipal()返回的是用户名

protected Object getAuthenticationCacheKey(AuthenticationToken token) {
	return token != null ? token.getPrincipal() : null;
}

但是在退出要清除认证信息缓存的时候,调用的是如下的方法来获取key,其中调用的getAvailablePrincipal方法返回的是principal对象,与登录时的key对应不上,会导致退出了,但认证缓存还在的问题,下次登录时依然会查缓存,而不是查数据库,这就导致了缓存与数据库的一致性问题。

protected Object getAuthenticationCacheKey(PrincipalCollection principals) {
    return getAvailablePrincipal(principals);
}

最重要的还是doGetAuthorizationInfo以及doGetAuthenticationInfo方法,二者分别返回授权信息以及认证信息,代码很简单,不过多解释,至于获取认证信息时使用的userPrincipalService.getUserPrincipal(username),看后续的代码即可。除此之外,重写了supports方法,此方法指明了当前Realm支持的认证token类型,官方更建议使用setAuthenticationTokenClass来进行设置,只不过这里只有一种认证方式,所以直接指定只支持用户名密码认证即可。

如果自定义鉴权规则,重写对应的isPermitted方法即可。

UserPrincipalEntity定义了用户主要信息,包括用户id、用户名、密码、登录时间、以及对应的角色编号和权限标识,代码如下。

@Accessors(chain = true)
@Data
public class UserPrincipalEntity implements Serializable {
    /**
     * 用户id
     */
    private Long id;
    /**
     * 用户名
     */
    private String username;
    /**
     * 加密后的密码
     */
    @JsonIgnore
    private String password;
    /**
     * 登录时间
     */
    private Date loginTime;
    /**
     * 角色
     */
    private Set<String> roles;
    /**
     * 权限
     */
    private Set<String> permissions;
}
public interface UserPrincipalService {
    UserPrincipalEntity getUserPrincipal(String username);
}
@RequiredArgsConstructor
@Service
public class UserPrincipalServiceImpl implements UserPrincipalService {

    private final UaUserInfoService userInfoService;
    private final UaUserRoleRelationService userRoleRelationService;
    private final UaRoleInfoService roleInfoService;
    private final UaRoleAuthorityRelationService roleAuthorityRelationService;
    private final UaAuthorityInfoService authorityInfoService;

    @Override
    public UserPrincipalEntity getUserPrincipal(String username) {
        UaUserInfo userInfo = userInfoService.getOne(new QueryWrapper<UaUserInfo>().lambda().eq(UaUserInfo::getName, username));
        if (userInfo == null) {
            return null;
        }
        UserPrincipalEntity userPrincipal = new UserPrincipalEntity();
        userPrincipal.setId(userInfo.getId())
                .setUsername(userInfo.getName())
                .setPassword(userInfo.getPassword())
                .setRoles(new HashSet<>())
                .setPermissions(new HashSet<>())
                .setLoginTime(new Date());
        Set<Long> roleIds = userRoleRelationService.list(new QueryWrapper<UaUserRoleRelation>().lambda().eq(UaUserRoleRelation::getUserId, userInfo.getId()))
                .stream().map(UaUserRoleRelation::getRoleId)
                .collect(Collectors.toSet());
        if (CollectionUtil.isEmpty(roleIds)) {
            return userPrincipal;
        }
        List<UaRoleInfo> roleInfos = roleInfoService.listByIds(roleIds);
        List<UaRoleAuthorityRelation> roleAuthorityRelations = roleAuthorityRelationService.list(new QueryWrapper<UaRoleAuthorityRelation>()
                .lambda().in(UaRoleAuthorityRelation::getRoleId, roleIds));
        Set<Long> authorityIds = roleAuthorityRelations.stream().map(UaRoleAuthorityRelation::getAuthorityId)
                .collect(Collectors.toSet());
        List<UaAuthorityInfo> authorityInfos;
        if (CollectionUtil.isEmpty(authorityIds)) {
            authorityInfos = new ArrayList<>();
        } else {
            authorityInfos = authorityInfoService.listByIds(authorityIds);
        }

        Set<String> roles = new HashSet<>();
        Set<String> permissions = new HashSet<>();
        for (UaRoleInfo role : roleInfos) {
            roles.add(role.getCode());
            Set<Long> ids = roleAuthorityRelations.stream()
                    .filter(item -> role.getId().equals(item.getRoleId()))
                    .map(UaRoleAuthorityRelation::getAuthorityId)
                    .collect(Collectors.toSet());
            Set<String> perms = authorityInfos.stream()
                    .filter(item -> ids.contains(item.getId()))
                    .map(UaAuthorityInfo::getUri)
                    .collect(Collectors.toSet());
            permissions.addAll(perms);
        }
        userPrincipal.setRoles(roles).setPermissions(permissions);
        return userPrincipal;
    }
}

Realm已经配置,接下来就是让它生效了。

4、shiro核心配置

先把代码贴出来,在一一解释

@Configuration
public class ShiroConfig {

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
        /*
         * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
         * 在@Controller注解的类的方法中加入@RequiresRole注解,会导致该方法无法映射请求,导致返回404。 加入这项配置能解决这个bug
         */
        proxyCreator.setProxyTargetClass(true);
        proxyCreator.setUsePrefix(false);
        return proxyCreator;
    }

    @Bean
    public EhCacheManager shiroCacheManager() {
        EhCacheManager ehCacheManager = new EhCacheManager();
        CacheManager cacheManager = CacheManager.create();
        ehCacheManager.setCacheManager(cacheManager);
        return ehCacheManager;
    }

    @Bean
    public DefaultWebSecurityManager securityManager(List<Realm> realms) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealms(realms);
        securityManager.setCacheManager(shiroCacheManager());
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);

        Map<String, Filter> filterMap = new LinkedHashMap<>();
        factoryBean.setFilters(filterMap);
        factoryBean.setFilterChainDefinitionMap(getFilterChainDefinitionMap());
        return factoryBean;
    }

    private Map<String, String> getFilterChainDefinitionMap() {
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/auth/login", "anon");
        filterChainDefinitionMap.put("/auth/registry", "anon");
        filterChainDefinitionMap.put("/auth/logout", "authc");
        filterChainDefinitionMap.put("/doc.html", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/swagger**", "anon");
        filterChainDefinitionMap.put("/v2/api-docs/**", "anon");
        filterChainDefinitionMap.put("/v3/api-docs/**", "anon");
        filterChainDefinitionMap.put("/error", "anon");
        filterChainDefinitionMap.put("/**", "anon");
        return filterChainDefinitionMap;
    }
}

首先是LifecycleBeanPostProcessor,注册这个Bean的目的在于将shiro的生命周期交给spring进行管理,无需用户手动干预。

DefaultAdvisorAutoProxyCreator主要是解决一个bug,如代码中注释所述,当controller接口上加了shiro的权限注解时,接口映射会有问题,报404,这个Bean就是用于解决此问题。

EhCacheManager中主要注意CacheManager.create(),通过查看此方法的源码可以发现,此方法使用了双重检查的单例模式来确保jvm中只有一个实例,这一点在上面的缓存配置那里有提及。在这里,如果不用CacheManager.create()而是ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml")则最终会导致ehcache实例不唯一而启动失败。

DefaultWebSecurityManagerSecurityManager的一个实现,在这里,注入了Realm集合,实际上就一个LoginRealm,同时设置了缓存管理器用来保存认证与鉴权信息。

ShiroFilterFactoryBean用来注册过滤器,getFilterChainDefinitionMap()方法返回的是默认的过滤规则,对于一些固定死的请求,可以在这里进行设置,filterChainDefinitionMap的键是请求路径,值则是对应的过滤器名称,这里使用的都是shiro默认过滤器的简写。shiro提供的默认过滤器如下

配置缩写对应的过滤器功能
anonAnonymousFilter指定url可以匿名访问
authcFormAuthenticationFilter指定url需要form表单登录,默认会从请求中获取usernamepassword,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。
authcBasicBasicHttpAuthenticationFilter指定url需要basic登录
logoutLogoutFilter登出过滤器,配置指定url就可以实现退出功能,非常方便
noSessionCreationNoSessionCreationFilter禁止创建会话
permsPermissionsAuthorizationFilter需要指定权限才能访问
portPortFilter需要指定端口才能访问
restHttpMethodPermissionFilter将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释
rolesRolesAuthorizationFilter需要指定角色才能访问
sslSslFilter需要https请求才能访问
userUserFilter需要已登录或“记住我”的用户才能访问

一般来说,上述的过滤器结合shiro的权限注解已经足够使用了,而如果需要自定义过滤器,可参考如下的示例代码(注:只是示例,并未用到)

首先,定义一个过滤器

public class CustomAuthFilter extends AuthenticatingFilter {

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        // 从请求头中获取对应的认证信息,此方法会在后面执行executeLogin时由父类调用
        // if (认证信息不为空) {
        //     return AuthenticationToken实例;
        // }
        return null;
    }


    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // 放过预检请求
        return ((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name()) || super.isPermissive(mappedValue);
    }


    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        // isAccessAllowed返回false,会进入此方法
        // 从请求头中获取对应的认证信息
        // if (认证信息不为空) {
        //     // 进行登录认证
        //     return executeLogin(request, response);
        // }
        // 否则当做匿名用户放行,毕竟有些接口是不登录也可以访问的,具体能否访问就看过滤规则以及接口上的权限注解了
        return true;
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        // executeLogin登录失败则返回错误信息
        writeResult(ApiResult.error(HttpStatus.UNAUTHORIZED, e.getMessage()));
        return false;
    }

    private <T> void writeResult(ApiResult<T> result) {
        HttpServletResponse response = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
        assert response != null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
        // 后台统一返回数据的状态码都是200(系统层面请求成功), 实际业务的状态码根据 ApiResult 进行判断
        response.setStatus(HttpStatus.OK.value());
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
            writer.print(JSONUtil.toJsonStr(result));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

然后注册过滤器,在上面的配置类中,修改最后两个方法如下

@Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);

        Map<String, Filter> filterMap = new LinkedHashMap<>();
        // 注册自定义过滤器
        filterMap.put("customAuthFilter", new CustomAuthFilter());
        factoryBean.setFilters(filterMap);
        factoryBean.setFilterChainDefinitionMap(getFilterChainDefinitionMap());
        return factoryBean;
    }

    private Map<String, String> getFilterChainDefinitionMap() {
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/auth/login", "anon");
        filterChainDefinitionMap.put("/auth/registry", "anon");
        filterChainDefinitionMap.put("/auth/logout", "anon");
        filterChainDefinitionMap.put("/doc.html", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/swagger**", "anon");
        filterChainDefinitionMap.put("/v2/api-docs/**", "anon");
        filterChainDefinitionMap.put("/v3/api-docs/**", "anon");
        filterChainDefinitionMap.put("/error", "anon");
        // 除上面定义的请求外,其余请求默认都会经过customAuthFilter过滤器
        // 注:这里的customAuthFilter名称要与上面注册的一致
        filterChainDefinitionMap.put("/**", "customAuthFilter");
        return filterChainDefinitionMap;
    }

至此,过滤器就OK了。不过我并未用到,上述仅仅是一个示例。接下来,对需要进行权限校验的接口,添加相应的注解,如列表查询用户的接口上添加@RequiresPermissions("list:ua-user-info")注解,则用户必须要拥有list:ua-user-info权限才可以访问。

相应的注解还有以下几个

注解功能
@RequiresGuest只有游客可以访问
@RequiresAuthentication需要登录才能访问
@RequiresUser已登录的用户或“记住我”的用户能访问
@RequiresRoles已登录的用户需具有指定的角色才能访问
@RequiresPermissions已登录的用户需具有指定的权限才能访问

接下来就是登录退出功能的实现了。

5、登录退出

登录退出在shiro非常简单,登录只需要SecurityUtils.getSubject().login(token)即可,其中的tokenAuthenticationToken实现类实例,不同类型的AuthenticationToken实例对应不同的Realm,退出则SecurityUtils.getSubject().logout()即可。而如果是在过滤器中进行认证,则参考上面的过滤器示例中executeLogin方法。

新建类LoginController,代码如下

@Api(tags = "认证")
@RequiredArgsConstructor
@Validated
@RequestMapping("auth")
@RestController
public class LoginController {

    private final UaUserInfoService userInfoService;

    @ApiOperation("注册")
    @PostMapping("registry")
    public ApiResult<UaUserInfo> registry(@RequestBody @Valid LoginRegistryParam param) {
        long count = userInfoService.count(new QueryWrapper<UaUserInfo>().lambda().eq(UaUserInfo::getName, param.getUsername()));
        if (count > 0) {
            return ApiResult.error(HttpStatus.BAD_REQUEST, "用户已存在!");
        }
        String encryptPassword = userInfoService.encryptPassword(param.getPassword());
        UaUserInfo userInfo = new UaUserInfo()
                .setName(param.getUsername())
                .setPassword(encryptPassword);
        userInfoService.save(userInfo);
        return ApiResult.ok(userInfo);
    }

    @ApiOperation("登录")
    @PostMapping("login")
    public ApiResult<UserPrincipalEntity> login(@RequestBody @Valid LoginRegistryParam param) {
        UsernamePasswordToken token = new UsernamePasswordToken(param.getUsername(), param.getPassword());
        SecurityUtils.getSubject().login(token);
        UserPrincipalEntity userPrincipal = (UserPrincipalEntity) SecurityUtils.getSubject().getPrincipal();
        return ApiResult.ok(userPrincipal);
    }

    @ApiOperation("退出")
    @PostMapping("logout")
    public ApiResult<Void> logout() {
        SecurityUtils.getSubject().logout();
        return ApiResult.ok();
    }
}

其中,LoginRegistryParam的定义如下

@Data
public class LoginRegistryParam {

    @NotBlank
    private String username;

    @NotBlank
    private String password;
}

为了方便测试,我预先准备了一份脚本,创建了admin用户并绑定角色ADMIN,同时为ADMIN授予了所有接口的权限。脚本如下

truncate table ua_role_info;
insert into ua_role_info(code, name)
VALUES ('ADMIN', '系统管理员'),
       ('USER', '普通用户');


truncate table ua_authority_info;
insert into ua_authority_info(id, parent_id, name, uri, type)
VALUES (1, -1, '认证授权', 'ua', 1),
       (2, 1, '权限管理', 'ua-authority-info', 1),
       (3, 1, '角色管理', 'ua-role-info', 1),
       (4, 1, '用户管理', 'ua-user-info', 1);

insert into ua_authority_info(parent_id, name, uri, type, group_name)
VALUES (2, '列表查询', 'list:ua-authority-info', 2, 'ua-authority-info'),
       (2, '主键查询', 'detail:ua-authority-info', 2, 'ua-authority-info'),
       (2, '新增', 'save:ua-authority-info', 2, 'ua-authority-info'),
       (2, '更新', 'update:ua-authority-info', 2, 'ua-authority-info'),
       (2, '删除', 'delete:ua-authority-info', 2, 'ua-authority-info');

insert into ua_authority_info(parent_id, name, uri, type, group_name)
VALUES (3, '列表查询', 'list:ua-role-info', 2, 'ua-role-info'),
       (3, '主键查询', 'detail:ua-role-info', 2, 'ua-role-info'),
       (3, '新增', 'save:ua-role-info', 2, 'ua-role-info'),
       (3, '更新', 'update:ua-role-info', 2, 'ua-role-info'),
       (3, '删除', 'delete:ua-role-info', 2, 'ua-role-info');
insert into ua_authority_info(parent_id, name, uri, type, group_name)
VALUES (3, '列表查询', 'list:ua-role-authority-relation', 2, 'ua-role-authority-relation'),
       (3, '主键查询', 'detail:ua-role-authority-relation', 2, 'ua-role-authority-relation'),
       (3, '新增', 'save:ua-role-authority-relation', 2, 'ua-role-authority-relation'),
       (3, '更新', 'update:ua-role-authority-relation', 2, 'ua-role-authority-relation'),
       (3, '删除', 'delete:ua-role-authority-relation', 2, 'ua-role-authority-relation');

insert into ua_authority_info(parent_id, name, uri, type, group_name)
VALUES (4, '列表查询', 'list:ua-user-info', 2, 'ua-user-info'),
       (4, '主键查询', 'detail:ua-user-info', 2, 'ua-user-info'),
       (4, '新增', 'save:ua-user-info', 2, 'ua-user-info'),
       (4, '更新', 'update:ua-user-info', 2, 'ua-user-info'),
       (4, '删除', 'delete:ua-user-info', 2, 'ua-user-info');
insert into ua_authority_info(parent_id, name, uri, type, group_name)
VALUES (4, '列表查询', 'list:ua-user-role-relation', 2, 'ua-user-role-relation'),
       (4, '主键查询', 'detail:ua-user-role-relation', 2, 'ua-user-role-relation'),
       (4, '新增', 'save:ua-user-role-relation', 2, 'ua-user-role-relation'),
       (4, '更新', 'update:ua-user-role-relation', 2, 'ua-user-role-relation'),
       (4, '删除', 'delete:ua-user-role-relation', 2, 'ua-user-role-relation');

truncate table ua_user_info;
insert into ua_user_info(id, name, password)
    VALUE (1, 'admin', 'i8TY37+scxcO3FrMuHVnDaULwDc11+ujgGXPRG5YBWs=');

truncate table ua_user_role_relation;
insert into ua_user_role_relation(user_id, role_id)
VALUES (1, 1);

truncate table ua_role_authority_relation;
insert into ua_role_authority_relation(role_id, authority_id)
select 1, id
from ua_authority_info;

启动项目,然后登录即可访问对应接口,如果是新注册用户或者是权限未被授予,则无法访问。

6、异常处理

异常处理,包括认证授权异常、参数校验异常以及如空指针异常等这类运行时异常。代码如下,其中,账号被锁、操作频繁暂时没有用到

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResult<Void> handleValidationException(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        StringBuilder msg = new StringBuilder();
        if (bindingResult instanceof BeanPropertyBindingResult) {
            for (ObjectError error : bindingResult.getAllErrors()) {
                if (error instanceof FieldError) {
                    msg.append(((FieldError) error).getField()).append(error.getDefaultMessage()).append("!");
                }
            }
        }
        return ApiResult.error(HttpStatus.BAD_REQUEST, msg.toString());
    }

    @ExceptionHandler(UnknownAccountException.class)
    public ApiResult<Void> handleUnknownAccountException(UnknownAccountException e) {
        log.error("UnknownAccountException: {}", e.getMessage());
        return ApiResult.error(HttpStatus.UNAUTHORIZED, "账户不存在!");
    }

    @ExceptionHandler(IncorrectCredentialsException.class)
    public ApiResult<Void> handleIncorrectCredentialsException(IncorrectCredentialsException e) {
        log.error("IncorrectCredentialsException: {}", e.getMessage());
        return ApiResult.error(HttpStatus.UNAUTHORIZED, "密码错误!");
    }

    @ExceptionHandler(LockedAccountException.class)
    public ApiResult<Void> handleLockedAccountException(LockedAccountException e) {
        log.error("LockedAccountException: {}", e.getMessage());
        return ApiResult.error(HttpStatus.FORBIDDEN, "账号被锁定!");
    }

    @ExceptionHandler(ExcessiveAttemptsException.class)
    public ApiResult<Void> handleExcessiveAttemptsException(ExcessiveAttemptsException e) {
        log.error("LockedAccountException: {}", e.getMessage());
        return ApiResult.error(HttpStatus.FORBIDDEN, "操作频繁,请稍后再试!");
    }

    @ExceptionHandler(UnauthenticatedException.class)
    public ApiResult<Void> handleUnauthenticatedException(UnauthenticatedException e) {
        log.error("UnauthenticatedException: {}", e.getMessage());
        return ApiResult.error(HttpStatus.UNAUTHORIZED, "认证失败!");
    }

    @ExceptionHandler(UnauthorizedException.class)
    public ApiResult<Void> handleUnauthorizedException(UnauthorizedException e) {
        log.error("UnauthorizedException: {}", e.getMessage());
        return ApiResult.error(HttpStatus.FORBIDDEN, "没有访问权限!");
    }

    @ExceptionHandler(Exception.class)
    public ApiResult<Void> handleOtherException(Exception e) {
        log.error("server error: ", e);
        return ApiResult.error(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
    }
}

到此,就基本上结束了,当然还有不少可以优化的点,比如缓存,对于查询比较多的接口,可考虑使用@Cacheable之类的注解来进行缓存,另外,部分接口可考虑添加批量操作接口等。

代码已上传至gitee,见session分支:https://gitee.com/yang-guirong/shiro-boot/tree/session/

下一篇将讲述无状态的jwt配置过程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值