整合Shiro
Shiro是一款开源的轻量的权限管理框架,他能实现用户的认证和授权功能,也可以实现密码加密功能
基本使用
使用Shiro,首先需要导入starter依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.0</version>
</dependency>
然后我们创建一个实体类用户来承载认证信息
@Data
public class User{
private Integer id;
private String username;
private String password;
}
在数据库中添加两个用户
自定义一个Realm类来实现认证和授权,需要继承AuthorizingRealm
类并重写doGetAuthorizationInfo
方法和doGetAuthenticationInfo
方法
doGetAuthorizationInfo
方法用于授权,我们这里还不需要,先返回一个null
doGetAuthenticationInfo
方法用于认证,也就是实现登录认证和拦截的关键,这里注入mapper从数据库中查询登录信息的用户名,如果没有则返回null,Realm类会抛出UnknownAccountException
异常,然后使用一个SimpleAuthenticationInfo
类对用户的密码进行校验,如果校验不匹配,则会抛出AuthenticationException
异常
public class UserRealm extends AuthorizingRealm {
@Autowired
UserMapper userMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = authenticationToken.getPrincipal().toString();
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
if (user == null) {
return null;
}
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
}
下一步配置Shiro的拦截规则,依次注入自定义Realm类,securityManager类和ShiroFilterFactoryBean类,并且使用一个LinkedHashMap
类设置拦截路径,注意这里不能使用HashMap
类,因为HashMap
类是无序的,拦截的时候需要按照map中的元素顺序进行拦截,常见的拦截的规则有以下:
- anon:可匿名访问
- authc:需要认证才能访问
- perms[xxx]:用户必需拥有xxx权限才访问
- roles[xxx]:用户必需拥有xxx角色才访问
- perms[“1,2,3”]:用户必需拥有所有权限才访问
- roles[“1,2,3”]:用户必需拥有所有角色才访问
- logout:登出请求
并且可以使用通配符来匹配路径,?
匹配一个字符,*
匹配一级url或任意多个字符,**
匹配多级url或任意多个字符
@Configuration
public class ShiroConfig {
// 配置自定义Realm
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
// 配置securityManager并设置userRealm
@Bean
public DefaultWebSecurityManager securityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
// 设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new LinkedHashMap<>();
// 放行login请求
map.put("/login","anon");
// 设置登出url
map.put("/logout", "logout");
// 拦截其他请求
map.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
// 配置登录拦截跳转url
shiroFilterFactoryBean.setLoginUrl("/login_page");
return shiroFilterFactoryBean;
}
}
编写三个接口来测试登录认证,login为登录请求,test为测试请求,login_page为登录页面跳转请求
在登录逻辑中,使用Shiro的UsernamePasswordToken
类和SecurityUtils
类来进行登录验证,这样登录逻辑会跳到自定义Realm类中的doGetAuthenticationInfo
方法,然后捕获可能抛出的UnknownAccountException
异常和AuthenticationException
异常既可
@Controller
public class MyController {
@ResponseBody
@PostMapping("/login")
public String login(@RequestBody User user) {
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
try {
SecurityUtils.getSubject().login(token);
} catch (UnknownAccountException e) {
return "unknown user";
} catch (AuthenticationException e) {
return "password error";
}
return "success";
}
@ResponseBody
@GetMapping("/test")
public String test() {
return "login";
}
@GetMapping("/login_page")
public String loginPage(){
return "login_page.html";
}
}
编写一个简单的拦截跳转登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form action="/login" method="post">
username:<div><input type="text" name="username"/></div>
password:<div><input type="password" name="password"/></div>
<div><input type="submit" value="login"/></div>
</form>
</body>
</html>
最后测试一下,正常登录时:
客户端也获取到了Cookie
可以正常访问test接口
密码错误时:
用户名错误时:
如果未登录就访问test接口则会跳到登录页面
密码加密
以上的认证过程,密码是通过明文校验的,而且密码在数据库中也是使用明文保存,这样会很不安全
像密码这样的敏感数据,可以使用不可逆加密后再保存到数据库中,校验密码的时候只需要将明文密码通过相同的加密规则加密后再与数据库中的密文密码比对即可
Shiro为我们提供了几种密码加密规则,我们以Sha256作为示例
创建一个新建用户的接口,然后把密码加密之后再保存到数据库中,加密的时候需要一个盐值以防止彩虹表破解,我们用用户名作为加密的盐值
@Controller
public class MyController {
@Autowired
UserMapper userMapper;
@ResponseBody
@PostMapping("/addUser")
public String addUser(@RequestBody User user) {
String password = new Sha256Hash(user.getPassword(), user.getUsername(), 1024).toBase64();
user.setPassword(password);
userMapper.insert(user);
return "success";
}
}
调用接口添加两个用户
可以看到数据库中的密码都是加密后的密文数据
然后修改自定义Realm类中doGetAuthenticationInfo
方法的校验密码逻辑,在使用SimpleAuthenticationInfo
类进行校验是,将密码的盐值也传入构造方法
重写AuthorizingRealm
类的setCredentialsMatcher
方法
public class UserRealm extends AuthorizingRealm {
@Autowired
UserMapper userMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = authenticationToken.getPrincipal().toString();
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
if (user == null) {
return null;
}
return new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(username), getName());
}
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
// 设置加密类型
matcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
// 设置加密方式为base64
matcher.setStoredCredentialsHexEncoded(false);
// 设置散列迭代次数
matcher.setHashIterations(1024);
super.setCredentialsMatcher(matcher);
}
}
这样就能与数据库中的密文密码进行比对校验
权限分离
除了认证之外,Shiro还可以做授权的功能,也就是权限分离,除了管理账号之外,其他的用户只拥有一部分功能,可以访问一部分接口
Shiro的授权功能分为角色和权限两种维度,以下示例以角色维度演示
首先修改用户实体类和数据库,添加角色属性
@Data
public class User{
private Integer id;
private String username;
private String password;
private String role;
}
实际中也可以将角色单独分离成一张表,然后用一张中间表关联用户和角色的关系
然后重写自定义Realm类中的doGetAuthorizationInfo
方法,将对应的角色授权给当前用户
public class UserRealm extends AuthorizingRealm {
@Autowired
UserMapper userMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
User currentUser = (User) principalCollection.getPrimaryPrincipal();
info.addRole(currentUser.getRole());
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = authenticationToken.getPrincipal().toString();
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
if (user == null) {
return null;
}
return new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(username), getName());
}
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
matcher.setStoredCredentialsHexEncoded(false);
matcher.setHashIterations(1024);
super.setCredentialsMatcher(matcher);
}
}
在拦截规则中设置/test请求只有root角色可以访问,并且设置未授权的角色被拦截后的跳转url
@Configuration
public class ShiroConfig {
// 配置自定义Realm
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
// 配置securityManager并设置userRealm
@Bean
public DefaultWebSecurityManager securityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new LinkedHashMap<>();
map.put("/logout", "logout");
map.put("/login","anon");
// 只放行root角色
map.put("/test","roles[root]");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
shiroFilterFactoryBean.setLoginUrl("/login_page");
// 设置未授权跳转url
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
return shiroFilterFactoryBean;
}
}
添加一个接口作为未授权的角色被拦截后的跳转接口
@Controller
public class MyController {
@ResponseBody
@GetMapping("/unauthorized")
public String unauthorized() {
return "unauthorized";
}
}
测试使用普通用户登录,之后访问/test接口,可以被Shiro拦截,并且跳转到指定的url