shiro认证授权
认证逻辑
-
所有请求都会被shiro过滤器拦截,这是我们需要在过滤器中放行某些可以访问的公共资源,例如注册页面、登录页面;以及配置某些认证后才能访问的资源,例如只有登录后才能访问首页;自定义realm规则,将该规则设置进安全管理器
/**
* shiro 配置类
*/
@Configuration
public class ShiroConfig {
// 解决thymeleaf模板中shiro标签不起作用
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
// 创建shiroFilter
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
Map<String,String> map = new HashMap<String,String>();
// anon 设置为公共资源
map.put("/login.html","anon");
map.put("/register.html","anon");
map.put("/user/register","anon");
map.put("/user/login","anon");
// authc 请求这个资源需要认证和授权
map.put("/**","authc");
// 默认认证界面路径
shiroFilterFactoryBean.setLoginUrl("/user/login");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
// 创建安全管理器,将自定义的realm规则设置进安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
// 创建自定义realm,该realm与注册时的加密一样,用于登录使用同样的方式加密
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
// 修改凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法为md5
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
}
-
首先我们注册用户,该请求不会被拦截到达控制层,之后在业务层对明文密码进行md5加密加盐散列操作,同时将身份信息、加密后的凭证信息、随机盐保存到数据库
@Override
public void register(User user) {
// 生成随机盐
String salt = SaltUtils.getSalt(8);
// 将随机盐保存到数据
user.setSalt(salt);
// 明文密码进行md5 + salt + hash散列
Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
user.setPassword(md5Hash.toHex());
userDAO.save(user);
}
-
登录页面属于公共资源,之后再登录页面发送请求到达控制层,登录时的主身份信息和凭证信息会作为token到达安全管理器,凭证信息会使用注册时的加密逻辑进行加密
/**
* 控制层
*/
@RequestMapping("login")
@ResponseBody
public RespResult login(String username, String password,String code,HttpSession session,Model model) {
//比较验证码
String codes = (String) session.getAttribute("code");
try {
if (codes.equalsIgnoreCase(code)){
// 获取主体对象
Subject subject = SecurityUtils.getSubject();
// 生成令牌
subject.login(new UsernamePasswordToken(username, password));
return RespResult.success("");
}else{
throw new RuntimeException("验证码错误!");
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误!");
}catch (Exception e){
e.printStackTrace();
System.out.println(e.getMessage());
}
return RespResult.error("认证失败!");
}
-
之后会进入自定义的Realm,我们会重写doGetAuthenticationInfo方法,在该方法中会查询数据库中该主体的数据,并与token中数据对比,相同则表示认证成功,跳转到首页;
当前登录的数据封装成一个令牌,由安全管理器中的凭证管理器(credentialsMatcher)进行处理,与数据库中查询到的数据进行比较,凭证管理器会自动对登录数据进行加盐、加密、散列操作;因为在第一步的shiroConfig中已经设置过了
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取登录时的身份信息
String principal = (String) token.getPrincipal();
// 在工厂中获取service对象
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
// 根据身份信息获取注册时的数据
User user = userService.findByUserName(principal);
if(!ObjectUtils.isEmpty(user)){
return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),
new MyByteSource(user.getSalt()),
this.getName()); // this.name表示当前登录的主体
}
return null;
}
认证的具体实现
- 创建安全管理器
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
- 注入realm
- 设置加密逻辑
CustomerMd5Realm realm = new CustomerMd5Realm();
//设置realm使用hash凭证匹配器,因为注册时使用了md5加密,登录时同样需要使用该逻辑的操作
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//声明:使用的算法
credentialsMatcher.setHashAlgorithmName("md5");
//声明:散列次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
defaultSecurityManager.setRealm(realm);
- 将安全管理器注入安全工具类
SecurityUtils.setSecurityManager(defaultSecurityManager);
- 获取主体
- 将登录时的主身份信息和凭证信息作为token
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");
subject.login(token);
- 进入自定义realm中的doGetAuthenticationInfo方法,与查询到的用户名和密码进行比较
授权逻辑
- 每个主体拥有角色权限和资源权限,只有在认证成功后才能进行授权操作
- 在一个系统中前端资源或者后端路由上标注了具有某些角色权限和资源权限才能访问,当要访问这些资源时,都会进入自定义realm的doGetAuthorizationInfo方法中,根据登录时的主身份信息获取数据库中主体所拥有的角色权限和资源权限,并将权限赋给当前主体,之后与资源上指定的权限判断是否拥有所需的权限来决定是否可访问
授权的具体实现
- 在自定义realm的doGetAuthorizationInfo方法上打上断点,浏览器访问认证成功后才能访问的页面
- 在该方法中首先会获取主身份信息,根据主身份信息会去数据库查询该账号所拥有的角色权限和资源权限
- 将获取的权限添加到simpleAuthorizationInfo对象,那么该主体就可以访问对应的资源
/**
* 自定义的realm类
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("调用授权验证: "+primaryPrincipal);
// 根据主身份信息获取角色 和 权限信息
UserService userService = (UserService) ApplicationContextUtils
.getBean("userService");
User user = userService.findRolesByUserName(primaryPrincipal);
// 授权角色信息
if(!CollectionUtils.isEmpty(user.getRoles())){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
user.getRoles().forEach(role->{
simpleAuthorizationInfo.addRole(role.getName());
// 权限信息
List<Perms> perms = userService.findPermsByRoleId(role.getId());
if(!CollectionUtils.isEmpty(perms)){
perms.forEach(perm->{
simpleAuthorizationInfo.addStringPermission(perm.getName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}