将Shiro集成到Spring-Boot工程中
Shiro官网文档:
https://shiro.apache.org/spring-boot.html
导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.6.0</version>
</dependency>
编写自定义Realm
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* @MethodName doGetAuthorizationInfo
* @Description 权限配置类 拦截当前登录的用户权限
* @Param [principalCollection]
* @Return AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户名
String name = (String) principalCollection.getPrimaryPrincipal();
//查询用户名称
User user = userService.getUserByName(name);
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (User.Role role : user.getRoles()) {
//添加角色
simpleAuthorizationInfo.addRole(role.getRoleName());
//添加权限
for (User.Role.Permissions permissions : role.getPermissions()) {
simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
}
}
return simpleAuthorizationInfo;
}
/**
* @MethodName doGetAuthenticationInfo
* @Description 认证配置类 拦截登录
* @Param [authenticationToken]
* @Return AuthenticationInfo
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
return null;
}
//获取用户信息
String name = authenticationToken.getPrincipal().toString();
User user = userService.getUserByName(name);
if (user == null) {
//这里返回后会报出对应异常
return null;
} else {
//这里验证authenticationToken和simpleAuthenticationInfo的信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassWord(), getName());
return simpleAuthenticationInfo;
}
}
}
Shiro配置类
@Configuration
public class ShiroConfig {
// 配置自定义Realm
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(credentialsMatcher()); //配置使用哈希密码匹配
return userRealm;
}
// 配置url过滤器
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/captcha", "anon");
chainDefinition.addPathDefinition("/logout","anon");
chainDefinition.addPathDefinition("/layuiadmin/**", "anon");
chainDefinition.addPathDefinition("/druid/**", "anon");
chainDefinition.addPathDefinition("/api/**", "anon");
// all other paths require a logged in user
chainDefinition.addPathDefinition("/login","anon");
chainDefinition.addPathDefinition("/**", "authc");
return chainDefinition;
}
// 设置用于匹配密码的CredentialsMatcher
@Bean
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME); // 散列算法,这里使用更安全的sha256算法
credentialsMatcher.setStoredCredentialsHexEncoded(false); // 数据库存储的密码字段使用HEX还是BASE64方式加密
credentialsMatcher.setHashIterations(1024); // 散列迭代次数
return credentialsMatcher;
}
}
通过阅读
shiro-spring-boot-starter
依赖中的ShiroAutoConfiguration
类的源码可以得知springboot内会自动注册
Realm
类以及SecurityManager
,并且SecurityManager
会自动将这些Realm
管理起来,因此我们不需要执行SecurityManager.setRealms(realms);
只需要将自定义的MyRealm
注入到spring容器即可。如果我们不自己注册Realm到容器中,则会抛出 NoRealmBeanConfiguredException异常
同时springboot会自动注册
ShiroFilterFactoryBean
,并将spring容器中的ShiroFilterChainDefinition
和SecurityManager
管理起来,
因此我们也不需要执行filterFactoryBean.setFilterChainDefinitionMap(map);filterFactoryBean.setSecurityManager(this.securityManager);
我们只需要将ShiroFilterChainDefinition
注入到spring容器即可。注意:springboot虽然已经注册了默认的
ShiroFilterChainDefinition
,但url过滤器默认只配置了chainDefinition.addPathDefinition(" /** ", “authc”);,在日常开发中,光这一条url配置应该是不够的,因此通常我们需要自定义这个bean。
soringboot还会自动将容器中的Filter过滤器由ShiroFilterFactoryBean管理,所以当我们需要自定义权限管理过滤器时,只需要继承
FormAuthenticationFilter
类,并添加自己的认证逻辑,如验证码等,然后将自定义的过滤器注入容器即可,无需执行filterFactoryBean.setFilters(filterMap);
自定义认证过滤器
@WebFilter
public class MyFormAuthenticationFilter extends FormAuthenticationFilter{
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 在这里进行验证码的校验
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpSession session = httpServletRequest.getSession();
// 取出验证码
String validateCode = (String) session.getAttribute("validateCode");
// 取出页面的验证码
// 输入的验证和session中的验证进行对比
String randomcode = httpServletRequest.getParameter("code");
if (randomcode != null && validateCode != null && !randomcode.equals(validateCode)) {
// 如果校验失败,将验证码错误失败信息,通过shiroLoginFailure设置到request中
httpServletRequest.setAttribute("shiroLoginFailure", "kaptchaValidateFailed");//自定义登录异常
// 拒绝访问,不再校验账号和密码
return true;
}
return super.onAccessDenied(request, response);
}
}
Shiro默认登录页为/login.jsp,需要在项目配置文件application.yml中修改默认登录页等配置
shiro:
loginUrl: /login
successUrl: /
unauthorizedUrl: /error/error
Controller请求
@RestController
public class UserController {
@GetMapping("login")
public String login(User user) {
if (StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassWord())) {
return "请输入用户名和密码!";
}
//用户认证信息
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
user.getUserName(),
user.getPassWord()
);
try {
//进行验证,这里可以捕获异常,然后返回对应信息
subject.login(usernamePasswordToken);
// subject.checkRole("admin");
// subject.checkPermissions("query", "add");
} catch (UnknownAccountException e) {
log.error("用户名不存在!", e);
return "用户名不存在!";
} catch (AuthenticationException e) {
log.error("账号或密码错误!", e);
return "账号或密码错误!";
} catch (AuthorizationException e) {
log.error("没有权限!", e);
return "没有权限";
}
return "login success";
}
@RequiresPermissions("query")
@GetMapping("/index")
public String index() {
return "query success!";
}
@RequiresPermissions("add")
@GetMapping("/add")
public String add() {
return "add success!";
}
Shiro默认过滤器
配置缩写 | 对应的过滤器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定url可以匿名访问 |
authc | FormAuthenticationFilter | 指定url需要登录认证,默认会从请求中获取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 | 需要已登录或“记住我”的用户才能访问 |
例:
//访问/home请求需要登录认证 chainDefinition.addPathDefinition("/home", "anon"); //访问/manager请求需要manager角色 chainDefinition.addPathDefinition("/manager", "roles[manager]"); //访问/manager/delate请求需要admin权限 chainDefinition.addPathDefinition("/manager/delate", "perms[admin]");
Shiro常用的权限控制注解
可以在Controller类上使用
注解 | 功能 |
---|---|
@RequiresGuest | 只有游客可以访问 |
@RequiresAuthentication | 需要登录才能访问 |
@RequiresUser | 已登录的用户或“记住我”的用户能访问 |
@RequiresRoles | 已登录的用户需具有指定的角色才能访问 |
@RequiresPermissions | 已登录的用户需具有指定的权限才能访问 |
URL和注解组合使用
Shiro可以使用url配置控制权限,也可以在控制器类上使用注解控制权限。同时使用两种配置方式灵活结合,才是适应不同应用场景的最佳实践。只用注解或只用url配置,都不够灵活,有时会很麻烦。思路是:
用url配置控制鉴权,实现粗粒度控制;用注解控制授权,实现细粒度控制
@RequireXXX注解可能的Bug
注意:解决spring aop和注解配置一起使用的bug。如果您在使用shiro注解配置的同时,引入了spring aop 的starter,会有一个奇怪的问题,导致shiro注解的请求,不能被映射,需加入以下配置:
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求, * 导致返回404。 加入这项配置能解决这个bug
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
//defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
注解权限
控制器上通过注解配置详细的角色和权限,多个权限和角色之间默认是“与”关系,可以通过logical参数设置为“或”。
@RequiresRoles( value = {"admin", "user"}, logical = Logical.OR)
@GetMapping("/getRoles")
public String getRoles() {
return "getRoles success!";
}
缓存
启用缓存只需要提供提供CacheManager bean:
@Bean
protected CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
Shiro-Spring配置属性
Key | Default Value | Description |
---|---|---|
shiro.enabled | true | 启用Shiro的Spring模块 |
shiro.web.enabled | true | 启用Shiro的Spring Web模块 |
shiro.annotations.enabled | true | 为Shiro的注释启用Spring支持 |
shiro.sessionManager.deleteInvalidSessions | true | 从会话存储中删除无效的会话 |
shiro.sessionManager.sessionIdCookieEnabled | true | 启用会话ID到cookie,以进行会话跟踪 |
shiro.sessionManager.sessionIdUrlRewritingEnabled | true | 启用会话URL重写支持 |
shiro.userNativeSessionManager | false | 如果启用,Shiro将管理HTTP会话而不是容器 |
shiro.sessionManager.cookie.name | JSESSIONID | 会话Cookie名称 |
shiro.sessionManager.cookie.maxAge | -1 | 会话Cookie的最大生命 |
shiro.sessionManager.cookie.domain | null | 会话Cookie域 |
shiro.sessionManager.cookie.path | null | 会话Cookie路径 |
shiro.sessionManager.cookie.secure | false | 会话Cookie安全标志 |
shiro.rememberMeManager.cookie.name | rememberMe | RememberMe cookie名称 |
shiro.rememberMeManager.cookie.maxAge | one year | RememberMe Cookie的最大年龄 |
shiro.rememberMeManager.cookie.domain | null | RememberMe Cookie域 |
shiro.rememberMeManager.cookie.path | null | RememberMe Cookie路径 |
shiro.rememberMeManager.cookie.secure | false | RememberMe cookie安全标志 |
shiro.loginUrl | /login.jsp | 未经身份验证的用户重定向到登录页面时使用的登录URL |
shiro.successUrl | / | 用户登录后的默认登录页面(如果在当前会话中找不到替代登录页面) |
shiro.unauthorizedUrl | null | 用于将用户重定向到未经授权的页面(403错误) |