在上一篇中,搭建了项目的基础结构,这一篇中,将简述一下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
有三大核心组件,分别是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");
// 密码比对器 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
,此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 {
/**
* 用户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
实例不唯一而启动失败。
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 {
// 从请求头中获取对应的认证信息,此方法会在后面执行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)
即可,其中的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
配置过程。