springboot集成用户认证授权框架shiro基础教程
# springboot 集成用户认证授权框架 shiro 基础教程
本文介绍 springboot 项目集成 shiro 框架的步骤,使系统支持用户认证和授权。java 生态中,常用的用户认证授权框架有 2 个:spring security、shiro。而 shiro 因为使用相对简单, 且能满足大部分需求而成为首选的权限框架。shiro 对资源的认证和授权配置非常灵活, 支持全局配置(通过配置类)和局部配置(通过注解)。
提示
本文只讲述最基础的配置,不说废话,带您快速入门。
# 1. 安装依赖
// 权限框架shiro
compile 'org.apache.shiro:shiro-spring-boot-web-starter:1.6.0'
# 2. 创建 UserRealm 类
UserRealm 类做的工作只有 2 个:
认证
在函数 doGetAuthenticationInfo 中实现用户认证逻辑。 何时执行: 执行后文提到的 ShiroServiceImpl.login 方法会触发认证逻辑。授权
在函数 doGetAuthorizationInfo 中实现授权逻辑。
何时执行: 只有被访问的资源需要授权访问时, 才执行授权逻辑。
package com.ruiboyun.facehr.permission.shiro;
import cn.hutool.core.util.ObjectUtil;
import com.ruiboyun.facehr.permission.service.IPermissionService;
import com.ruiboyun.facehr.user.entity.User;
import com.ruiboyun.facehr.user.service.IUserService;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
@Log4j2
public class UserRealm extends AuthorizingRealm {
@Autowired
private IUserService iUserService;
@Autowired
private IPermissionService iPermissionService;
/**
* shiro授权
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 从shiro取出当前用户对象
User user = (User) principals.getPrimaryPrincipal();
// 从数据库查询出当前用户的角色列表和权限列表
List<String> permissionStringList = iPermissionService.listPermissionStringByUsername(user.getUsername());
List<String> roleNameList = iPermissionService.listRoleNameByUsername(user.getUsername());
// 将当前用户的角色和权限列表赋给shiro
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addStringPermissions(permissionStringList);
authorizationInfo.addRoles(roleNameList);
log.info("shiro授权, 完成, user[{}], permissionStringList[{}]", user, permissionStringList);
return authorizationInfo;
}
/**
* shiro认证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
User user = iUserService.getByUsername(token.getUsername());
if (ObjectUtil.isNull(user)) {
log.error("shiro认证, 失败, 用户不存在, username[{}]", token.getUsername());
return null;
}
log.info("shiro认证, 完成, user[{}]", user);
return new SimpleAuthenticationInfo(
user,
// 注意是从数据库读取的密码,并不是从客户端传入的密码: 若对密码做了加密存储的话,这2个密码的值是不同的
user.getPassword().toCharArray(),
getName()
);
}
}
您只需要根据具体需求, 改动如下逻辑: 根据用户名查询出用户的角色列表和权限列表。
这个逻辑您就可以任意发挥了,甚至不需要数据库,把数据写死都可以。
// 从数据库查询出当前用户的角色列表和权限列表
List<String> permissionStringList = iPermissionService.listPermissionStringByUsername(user.getUsername());
List<String> roleNameList = iPermissionService.listRoleNameByUsername(user.getUsername());
为了从简, 本教程假定密码为明文存储, 否则, UserRealm 类还需要做对应修改。
# 3. 创建 shiro 配置类
shiro 配置类完成的工作包括:
定义密码匹配器
若用户是加密的,则需要定义密码匹配器,否则无需定义。本教程没有定义。以"url 配置"方式初始化认证权限规则
需要依赖 shiro 提供的认证过滤器或权限过滤器,或者是用户自定义过滤器。
如前所述, 假定用户密码在数据库中没有加密存储。否则, 还需要再定义密码匹配器。
package com.ruiboyun.facehr.config;
import com.ruiboyun.facehr.permission.shiro.UserRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 权限框架shiro的配置
*/
@Configuration
public class ShiroConfig {
/**
* 自定义realm
*
* @return
*/
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
return userRealm;
}
/**
* 安全管理器
*
* @return
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
return securityManager;
}
/**
* 设置过滤规则
*
* @param defaultWebSecurityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
/**
* 资源需要认证, 且认证失败时, 跳转的url
* 注意: 如果不设置,默认会自动寻找Web工程根目录下的"/login.jsp"页面或"/login"映射
*/
shiroFilterFactoryBean.setLoginUrl("http://demo.com/login/");
// 资源需要授权, 且授权失败时, 跳转的url
shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");
//指定路径和过滤器的对应关系
//注意此处使用的是LinkedHashMap,是有顺序的,shiro会按从上到下的顺序匹配验证,匹配了就不再继续验证
//所以上面的url要苛刻,宽松的url要放在下面,尤其是"/**"要放到最下面,如果放前面的话其后的验证规则就没作用了。
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/api/user/user/loginByPhoneAndPassword", "anon");
// 其它请求都需要认证
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
}
提示
将如上配置类复制到您的工程中,需要对如下 3 处按实际情况修改:
- shiroFilterFactoryBean.setLoginUrl("http://demo.com/login/")
- shiroFilterFactoryBean.setUnauthorizedUrl("/unauth")
- filterChainDefinitionMap.put("/api/user/user/loginByPhoneAndPassword", "anon")
若项目中集成了 swagger,则需要对 swagger api 文档的资源访问路径做"匿名访问"配置:
/************** start swagger接口文档支持匿名访问 ***************/
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
/************** end swagger接口文档支持匿名访问 ***************/
# 4. 创建 ShiroService
为了代码清晰, 我们创建一个独立的 ShiroService 类, 用于封装 shiro 的登录和登出逻辑。
package com.ruiboyun.facehr.permission.service.impl;
import com.ruiboyun.facehr.permission.service.IShiroService;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Service;
@Service
@Log4j2
public class ShiroServiceImpl implements IShiroService {
/**
* shiro登录
*
* @param username
* @param password
* @return
*/
@Override
public void login(String username, String password) {
Subject user = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
user.login(token);
log.info("shiro登录, 完成, username[{}], password[{}]", username, password);
}
/**
* shiro登出
*/
@Override
public void logout() {
Subject user = SecurityUtils.getSubject();
user.logout();
log.info("shiro登出, user[{}]", user);
}
}
# 5. 修改业务逻辑
修改登录接口, 补充逻辑: 调用 ShiroServiceImpl.login 方法。
修改登出接口, 补充逻辑: 调用 ShiroServiceImpl.logout 方法。
# 6. 定义请求的认证和授权规则
请求的认证和授权规则, 有 2 种配置方式:
配置方式 | 实现方法 | 控制精度 | 灵活性 | 认证或授权失败的处理方式 |
---|---|---|---|---|
通过"url 配置" | 修改 ShiroConfig.shiroFilterFactoryBean 方法, 追加认证和授权规则。 | 粗粒度控制 | 支持动态配置 | 跳转到配置中指定的 url |
通过注解 | 在接口定义方法上写注解 | 细粒度地控制。 | 因为注解是写死到代码中,无法动态配置 | 抛出异常。为了更友好的给用户提供错误信息,建议在全局异常捕获处理器中捕获认证或授权的所有异常,并以友好的方式返回给用户 |
为了能够捕获"注解方式"的认证或授权异常, 需要在全局异常处理类中补充如下异常处理函数:
/**
* shiro认证异常
* Shiro在登录认证过程中,认证失败需要抛出的异常
*
* @param ex
* @return
*/
@ExceptionHandler(value = AuthenticationException.class)
public Result handleAuthenticationException(AuthenticationException ex) {
// 凭证异常
if (ex instanceof CredentialsException) {
// 不正确的凭证
if (ex instanceof IncorrectCredentialsException) {
log.error("shiro认证异常, 凭证异常, 不正确的凭证");
return Result.error("账号或密码错误");
} else if (ex instanceof ExpiredCredentialsException) {
log.error("shiro认证异常, 凭证异常, 凭证过期");
return Result.error("账号或密码错误");
}
log.error("shiro认证异常, 凭证异常");
return Result.error("账号或密码错误");
}
//账号异常
else if (ex instanceof AccountException) {
// 并发访问异常: 多个用户同时登录时抛出
if (ex instanceof ConcurrentAccessException) {
log.error("shiro认证异常, 账号异常, 并发访问异常");
return Result.error("不允许多个用户同时登录");
} else if (ex instanceof UnknownAccountException) {
log.error("shiro认证异常, 账号异常, 未知的账号");
return Result.error("账号或密码错误");
} else if (ex instanceof ExcessiveAttemptsException) {
log.error("shiro认证异常, 账号异常, 认证次数超过限制");
return Result.error("登录次数超过限制");
} else if (ex instanceof DisabledAccountException) {
log.error("shiro认证异常, 账号异常, 禁用的账号");
return Result.error("账号已禁用");
} else if (ex instanceof LockedAccountException) {
log.error("shiro认证异常, 账号异常, 账号被锁定");
return Result.error("账号被锁定");
}
log.error("shiro认证异常, 账号异常");
return Result.error("账号异常");
}
// 使用了不支持的Token
else if (ex instanceof UnsupportedTokenException) {
log.error("shiro认证异常, 使用了不支持的Token");
return Result.error("认证失败");
}
log.error("shiro认证异常");
return Result.error("您没有登录,无权执行此操作");
}
/**
* shiro授权异常
*
* @param ex
* @return
*/
@ExceptionHandler(value = AuthorizationException.class)
public Result handleAuthorizationException(AuthorizationException ex) {
if (ex instanceof UnauthorizedException) {
log.error("shiro授权异常, 无权访问");
return Result.error("无权访问");
}
//当尚未完成成功认证时, 尝试执行授权操作时引发该异常
else if (ex instanceof UnauthenticatedException) {
log.error("shiro授权异常, 没有通过认证无法执行授权操作");
return Result.error("请先登录");
}
log.error("shiro授权异常");
return Result.error("无权访问");
}
# 7. 验证
完成了如上步骤以后, 就完成了 shiro 的基本集成工作。可以进行功能验证了。
# 参考资料
Spring Boot2 整合 Shiro - 仅支持身份认证 (opens new window)
Spring Boot2 整合 Shiro - 密码加密存储 (opens new window)
https://blog.csdn.net/gnail_oug/article/details/80662553
https://segmentfault.com/a/1190000014479154
https://www.cnblogs.com/seve/p/12241197.html
原生和 starter 整合 shiro 的区别 (opens new window)