整合思路
ShiroFilter会拦截所有请求,Shrio会判断哪些请求需要做认证和授权,哪些不需要做。
如果请求中访问的是系统的公共资源,则不需要进行认证和授权的操作,ShiroFilter直接放行即可。
如果请求中访问的是系统的受限资源,若第一次访问需要做认证,认证成功后,后续的访问进行授权。ShiroFilter依赖SecurityManager来完成认证和授权的具体操作,同时SecurityManager也依赖Realm来获取认证和授权的相应数据。
公共资源不需要认证和授权,任何用户都能访问。似于登录页面,注册页面。
受限资源是需要认证成功并赋予权限才能访问的资源。类似于系统主页,用户主页。
如果Shiro整合Spring Cloud,则将相应操作整合进Spring Cloud Gateway或者zuul即可。
整合Shiro实现认证
1、pom.xml
中引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
2、创建工厂工具类
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
// 工厂就是该方法的参数,当Spring Boot启动时,该参数就会接收到工厂
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
context = applicationContext;
}
// 根据Bean的名字获取工厂中指定对象
public static Object getBean(String beanName) {
return applicationContext.getBean(beanName);
}
}
3、构建shiro包,在shiro包下构建realms包
4、realms包中构建自定义Realm
public class UserRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
// 认证操作
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取前端传入身份信息
String username = (String) token.getPrincipal();
// 从工厂中获取userService
UserSerivce userSerivce = (UserSerivce) ApplicationContextUtils.getBean("userService");
// 根据身份信息从DB中获取User
User user = userSerivce.getUserByUsername(username);
// 获取加密后的密码和Salt,Shiro自动进行认证
if (user != null) {
return new SimpleAuthenticationInfo(username, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
}
return null;
}
}
默认被Spring工厂托管的Bean的名字都是其类名首字母小写,也可以指定,比方说@Service("userService")。
5、创建Shiro配置类
@Configuration
public class ShiroConfig {
// 创建ShiroFilter
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 给ShiroFilter注入SecurityManager
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 设置默认认证路径,认证失败后会调用该接口,也算是公共资源
shiroFilterFactoryBean.setLoginUrl("/user/login");
// 配置公共资源和受限资源
Map<String, String> map = new HashMap<>();
// anon是过滤器的一种,表示该资源是公共资源,需要设置在authc上面
map.put("/user/register", "anon");
map.put("/user/login", "anon");
// authc是过滤器的一种,表示除了设置公共资源和默认认证路径之外所有资源是受限资源
map.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
// 创建具有Web特性的SecurityManager
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("getRealm") Realm realm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 给SecurityManager注入Realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
// 创建自定义Realm
@Bean
public Realm getRealm() {
UserRealm userRealm = new UserRealm();
// 设置Hash凭证校验匹配器,用来完成密码加密校验
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法MD5
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 设置散列次数1024
hashedCredentialsMatcher.setHashIterations(1024);
// 注入凭证校验匹配器
userRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return userRealm;
}
}
6、设计数据库
user表中需要在基础上添加salt字段。
7、创建随机生成Salt的工具类
public class SaltUtils {
/**
* 随机生成定长的Salt
* @param n 长度
* @return Salt
*/
public static String getSalt(int n) {
char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~!@#$%^&*()_+|{}:.,<>?/".toCharArray();
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < n; i++) {
char c = chars[new Random().nextInt(chars.length)];
stringBuilder.append(c);
}
return stringBuilder.toString();
}
}
8、创建Controller
默认DB、MP和Service都已经配置并编写完毕。
以下代码为了方便展示,将业务写在Controller中,实际开发时需要提取进Service。
@RestController
@RequestMapping("/user")
public class UserContoller {
@Autowired
private UserService userService;
@PostMapping("register")
public Response register(@RequestBody UserRegisterDto userRegisterDto) {
try {
// 生成8位Salt
String salt = SaltUtils.getSalt(8);
// MD5 + Hash + Salt给密码加密
Md5Hash md5Hash = new Md5Hash(userRegisterDto.getPassword(), salt, 1024);
// 注册
userService.register(userRegisterDto.getUsername(), md5Hash.toHex(), salt);
// 注册成功
return Response.ok().message("注册成功");
} catch (Exception e) {
return Response.error(ResponseEnum.UNIFIED_ERROR).message("注册失败");
}
}
@PostMapping("login")
public Response login(@RequestBody UserLoginDto userLoginDto) {
Subject subject = SecurityUtils.getSubject();
try {
// 登录,Shiro自动认证
subject.login(new UsernamePasswordToken(userLoginDto.getUsername(), userLoginDto.getPassword()));
// 认证成功
return Response.ok().message("登录成功");
} catch (UnknownAccountException e) {
return Response.error(ResponseEnum.UNKNOWN_ACCOUNT_ERROR);
} catch (IncorrectCredentialsException e) {
return Response.error(ResponseEnum.INCORRECT_CREDENTIALS_ERROR);
}
}
@GetMapping("logout")
public Response login() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return Response.ok().message("退出成功");
}
}
在Web环境中,只要Shiro配置类中配置了SecurityManager,那么Spring就会将其托管,无需在Controller中单独创建。
Shiro过滤器
Shiro提供多个默认的过滤器,我们可以用这些过滤器来配置控制指定URL的权限。
常用的有两种:anon和authc。
过滤器缩写 | 过滤器 | 功能 |
anon | AnonymousFilter | 指定URL可以匿名访问,无需认证和授权 |
authc | FormAuthenticationFilter | 指定URL需要form表单登录,默认会从请求中获取username,password , rememberMe等参数并尝试登录,如果登录不了就会跳转到setLoginUrl配置的认证路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,因为可以定制出错返回的信息。 |
authcBasic | BasicHttpAuthenticationFilter | 指定URL需要basic登录 |
logout | LogoutFilter | 登出过滤器,配置指定URL就可以实现退出功能,非常方便 |
noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
perms | PermissionsAuthorizationFilter | 需要指定权限才能访问 |
port | PortFilter | 需要指定端口才能访问 |
rest | HttpMethodPermissionFilter | 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 |
roles | RolesAuthorizationFilter | 需要指定角色才能访问 |
ssl | SslFilter | 需要https请求才能访问 |
user | UserFilter | 需要已登录或 "记住我" 的用户才能访问 |
整合Shiro实现授权
前文说了,Shiro提供了三种授权方式,在前后端分离的系统中,我们主要使用注解式实现授权。后端只负责写接口传递用户的权限信息,具体前台如何显示由前端负责。
1. @RequiresRoles注解
该注解标注在接口方法上,表示是指定的角色才可以访问该接口。
@GetMapping("/findByRole")
@RequiresRoles("admin")
public String findByRole() {
...
}
也可以设置多个角色,表示同时具有指定的所有角色才能访问该接口。
@GetMapping("/findAllByRole")
@RequiresRoles({"admin", "user"})
public String findAllByRole() {
...
}
2. @RequiresPermissions注解
该注解标注在接口方法上,表示有指定访问权限才可以访问该接口。
@GetMapping("/findByPermission")
@RequiresPermissions("user:*:*")
public String findByPermission() {
...
}
也可以设置多个访问权限,表示同时具有指定的所有访问权限才能访问该接口。
@GetMapping("/findAllByPermission")
@RequiresPermissions(value = {"user:*:*", "user:find:1"})
public String findAllByPermission() {
...
}
3. 授权数据持久化
在实际项目中,权限数据需要在DB中获,因此我们要设计角色表和权限表。
通常情况下,一般是这样设计的:用户 <—(* *)—> 角色,角色 <—(* *)—> 权限,权限 <—(1 1)—> 资源
- 设计用户表
- 设计角色表
表结构:
数据案例:
id | role |
1 | admin |
2 | user |
- 设计权限表
表结构:
permission为权限标识符,url为权限标识符对应的URL。
数据案例:
id | permission | url |
1 | user:*:* | |
2 | user:find:1 |
- 设计用户-角色表
表结构: - 设计角色-权限表
表结构:
4. 授权流程
- 构建Role和Permission的Bean
@Data
public class Role implements Serializable {
private String id;
private String role;
}
@Data
public class Permission implements Serializable {
private String id;
private String permission;
private String url;
}
所有的Bean必须序列化,因为后文要将该Bean存入Redis。
- 在User类中添加角色集合
@Data
public class User implements Serializable {
private String id;
private String username;
private String password;
private String salt;
private List<String> roles;
}
- 在Role类中添加权限集合
@Data
public class Role implements Serializable {
private String id;
private String role;
List<Permission> permissions;
}
- 在UserMapper和UserService中提供通过username查询单个用户的角色集合的接口
具体使用逻辑实现使用MP或者Mybatis不同,自行选择即可。
List<Role> getRolesByUsername(String username);
- 在UserMapper和UserService中提供通过role_id查询单个角色的权限集合的接口
List<Permission> getPermissionsByRoleId(String roleId);
具体使用逻辑实现使用MP或者Mybatis不同,自行选择即可。
- 整合Realm中授权的方法
public class UserRealm extends AuthorizingRealm {
// 授权操作
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取身份信息
String username = (String) principals.getPrimaryPrincipal();
// 从工厂中取出UserService
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
// 注入该角色的角色和权限
List<Role> roles = userService.getRolesByUsername(username);
if (roles != null) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(role -> {
// 注入角色
simpleAuthorizationInfo.addRole(role.getRole());
// 获取权限集合
List<Permission> permissions = userService.getPermissionsByRoleId(role.getId());
// 也可以使用该方法判断集合是否不为空
if (!CollectionUtils.isEmpty(permissions)) {
permissions.forEach(permission -> {
// 注入权限
simpleAuthorizationInfo.addStringPermission(permission.getPermission());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取前端传入身份信息
String username = (String) token.getPrincipal();
// 获取userService
UserSerivce userSerivce = (UserSerivce) ApplicationContextUtils.getBean("userService");
// 根据身份信息从DB中获取User
User user = userSerivce.getUserByUsername(username);
// 获取加密的密码和Salt,Shiro自动进行认证
if (user != null) {
return new SimpleAuthenticationInfo(username, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
}
return null;
}
}
- Controller接口根据角色验证
@GetMapping("/findAllByRole")
@RequiresRoles({"admin", "user"})
public String findAllByRole() {
System.out.println("具有 admin 和 user 角色");
return "具有 admin 和 user 角色";
}
- Controller接口根据权限验证
@GetMapping("/findAllByPermission")
@RequiresPermissions(value = {"user:*:*", "user:find:1"})
public String findAllByPermission() {
System.out.println("具有 user:*:* 和 product:*:* 权限");
return "具有 user:*:* 和 product:*:* 权限";
}