在上一篇中,搭建了项目的基础结构,这一篇中,将简述一下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
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="base_ehcache"/>
<defaultCache maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="600"
overflowToDisk="true"
maxElementsOnDisk="1000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<cache name="shiro-authentication-cache"
maxElementsInMemory="20000"
eternal="false"
overflowToDisk="true"
diskPersistent="false"
timeToLiveSeconds="1800"
diskExpiryThreadIntervalSeconds="7200"/>
<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 有三大核心组件,分别是Subject 、SecurityManager 以及Realm ,在web应用中,Subject 可理解为当前用户,SecurityManager 则是安全管理器,管理shiro 中的组件,Realm 则是为SecurityManager 提供认证授权信息。
shiro 已经提供了不少的Realm ,如JdbcRealm 、SimpleAccountRealm 等,但在实际使用中,还是自己定义一个比较方便。通常,一个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");
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();
}
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 ,此CacheManager 是shiro 的,不是spring cache 的,在此Realm 中开启了全局缓存,并设置认证缓存与授权缓存名称;同时,设置了密码比对器,与上一篇的UaUserInfoService 示例代码的加密方法对应。至于ShiroByteSource 这个类纯粹是为了弥补SimpleByteSource 未实现序列化接口导致无法缓存的问题,除非缓存失效,否则这个问题会一直存在。
另外,重写了getAuthenticationCacheKey 方法,因为登录时使用的AuthenticationToken 是UsernamePasswordToken ,登录后将认证信息缓存的时候,调用的是如下的方法来获取缓存的key ,而UsernamePasswordToken 的getPrincipal() 返回的是用户名
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 {
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();
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 实例不唯一而启动失败。
DefaultWebSecurityManager 是SecurityManager 的一个实现,在这里,注入了Realm 集合,实际上就一个LoginRealm ,同时设置了缓存管理器用来保存认证与鉴权信息。
ShiroFilterFactoryBean 用来注册过滤器,getFilterChainDefinitionMap() 方法返回的是默认的过滤规则,对于一些固定死的请求,可以在这里进行设置,filterChainDefinitionMap 的键是请求路径,值则是对应的过滤器名称,这里使用的都是shiro 默认过滤器的简写。shiro 提供的默认过滤器如下
配置缩写 | 对应的过滤器 | 功能 |
---|
anon | AnonymousFilter | 指定url可以匿名访问 | authc | FormAuthenticationFilter | 指定url需要form表单登录,默认会从请求中获取username 、password ,rememberMe 等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 | authcBasic | BasicHttpAuthenticationFilter | 指定url需要basic登录 | logout | LogoutFilter | 登出过滤器,配置指定url就可以实现退出功能,非常方便 | noSessionCreation | NoSessionCreationFilter | 禁止创建会话 | perms | PermissionsAuthorizationFilter | 需要指定权限才能访问 | port | PortFilter | 需要指定端口才能访问 | rest | HttpMethodPermissionFilter | 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 | roles | RolesAuthorizationFilter | 需要指定角色才能访问 | ssl | SslFilter | 需要https请求才能访问 | user | UserFilter | 需要已登录或“记住我”的用户才能访问 |
一般来说,上述的过滤器结合shiro 的权限注解已经足够使用了,而如果需要自定义过滤器,可参考如下的示例代码(注:只是示例,并未用到)
首先,定义一个过滤器
public class CustomAuthFilter extends AuthenticatingFilter {
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
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 {
return true;
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
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");
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");
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) 即可,其中的token 是AuthenticationToken 实现类实例,不同类型的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 配置过程。
|