Shiro是Apache公司推出一个权限管理框架,其内部封装了项目中的认证、授权、加密、会话等逻辑操作,通过这个框架可以简化项目中权限控制逻辑代码的编写。
Shiro框架概要架构
Shiro框架中主要通过Subject,SecuirtyManager,Realm对象完成认证和授权业务。
其中:
Subject此对象负责提交用户身份、权限等信息。
SecuirtyManager负责完成认证、授权等核心业务。
Realm负责通过数据逻辑对象获取数据库中的数据。
Shiro框架基础配置实现
添加依赖(添加此依赖之前,如果之前添加了Shiro-spring依赖,则要替换掉)
org.apache.shiro
shiro-spring-boot-web-starter
1.7.0
添加完依赖后启动会失败,还需做其它配置
1.创建一个Realm类型的实现类(基于此类通过dao访问数据库)
package com.cy.pj.sys.service.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class ShiroRealm extends AuthorizingRealm {
//此方法负责获取授权信息
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//此方法负责获取并封装认证信息
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
第二步 在项目启动类中添加Realm对象配置
@Bean
public Realm realm(){
return new ShiroRealm();
}
第三步在启动类中定义过滤规则(哪些访问路径进行认证才可以访问)
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
LinkedHashMapmap=new LinkedHashMap<>();
//设置允许匿名方法的资源路径(不需要登录即可访问)
map.put("/bower_components/**","anon");//key是路径 anon对应shiro中的一个匿名过滤器
map.put("/build/**","anon");
map.put("/dist/**","anon");
map.put("/plugins/**","anon");
//设置认证以后才可以访问的资源(注意这里的顺序,匿名访问的要放在上面)
map.put("/**","authc");//authc对应一个认证过滤器,表示认证以后才可以访问
chainDefinition.addPathDefinitions(map);
return chainDefinition;
}
第四步配置认证页面yml文件中(登录页面)
shiro:
loginUrl: /login.html
认证流程分析
1) 系统调用subject的login方法将用户信息提交给SecurityManager
2) SecurityManager将认证操作委托给认证器对象Authenticator
3) Authenticator将用户输入的身份信息传递给Realm。
4) Realm访问数据库获取用户信息然后对信息进行封装并返回。
5) Authenticator 对realm返回的信息进行身份认证。
其中:
1) token:封装用户提交的认证信息(例如用户名和密码)的一个对象
2) Subject:负责将认证信息提交给SecurityManager对象的一个主体对象
3) SecurityManager:是Shiro框架的核心,负责完成其认证过程
4) Authenticator:认证管理器对象,SecurityManager继承了此接口
5) realm:负责从数据库获取认证信息提交给认证管理器
Shiro认证流程总结
1.登录客户端(login.html)中用户输入的登录信息提交SysUserController对象
2.SysUserController基于doLogin方法处理登录请求
3.SysUserController中doLogin方法将用户信息封装token中,然后基于subject对象将token提交给SecuirtyManager对象
4.SecuirtyManager对象调用认证方法(authenticate)去完成认证,在此方法内部会调用ShiroRealm中的doGetAuthenticationInfo获取数据库中的用户信息,然后再与客户端提交的token中的信息进行比对。比对时会调用getCredentialsMatcher方法获取凭证加密对象,通过此对象对用户提交的token中的密码进行加密。
Shiro框架认证业务实现:
1.通过Dao数据逻辑层基于用户名查询用户信息
SysUser findUserByUsername(String username);
1.1Mapper映射文件的sql语句
2.修改ShiroRealm中获取认证信息的方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取用户提交的认证信息
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
//基于用户名从数据库查询用户信息
SysUser user = sysUserDao.findUserByUsername(username);
//判断用户是否存在
if (user == null) {
throw new UnknownAccountException("用户不存在");//Shiro中的自定义异常
}
//判断用户是否被锁定
if (user.getValid() == 0) {
throw new LockedAccountException("账户被锁定");//Shiro中的自定义异常
}
//封装认证信息并返回
ByteSource credentialsSalt=ByteSource.Util.bytes(user.getSalt());//对盐值的编码方式进行了处理
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(),credentialsSalt,"ShiroRealm");//传参内容:用户身份、已加密的密码、凭证盐、realm名字
return simpleAuthenticationInfo;//返回给认证管理器
}
3.在ShiroRealm中重写获取凭证加密算法的方法
@Override//获取凭证匹配器的方法
public CredentialsMatcher getCredentialsMatcher() {
HashedCredentialsMatcher mather=new HashedCredentialsMatcher();
mather.setHashAlgorithmName("MD5");//加密算法
mather.setHashIterations(1);//加密次数
// 要和保存业务的时候设置的一致
return mather;
}
4.在SysUserController中添加处理登录请求的方法
@PostMapping("doLogin")
public JsonResult doLoginUI(String username,String password) {
//将账号和密码封装token对象
UsernamePasswordToken token=new UsernamePasswordToken(username,password);//参考官网
//基于subject对象将token提交给securityManager
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
return new JsonResult("login ok");
}
5.在过滤配置中允许doLogin这个url匿名访问map.put("/user/doLogin","anon");
6.在全局异常处理类中添加对于登录异常的处理方法
@ExceptionHandler(ShiroException.class)
public JsonResult doShiroException(ShiroException e) {
JsonResult jsonResult = new JsonResult();
jsonResult.setState(0);
if (e instanceof UnknownAccountException) {
jsonResult.setMessage("用户不存在");
}else if (e instanceof IncorrectCredentialsException) {
jsonResult.setMessage("密码不正确");
} else if (e instanceof LockedAccountException) {
jsonResult.setMessage("账户被锁定");
} else if (e instanceof AuthorizationException) {
jsonResult.setMessage("没有权限");
} else {
jsonResult.setMessage("认证或授权失败");
}
return jsonResult;
}
7.在过滤配置中配置登出url操作
map.put("/doLogout", "logout");//logout是Shiro框架给出的一个登出过滤器
Shiro框架授权业务的实现
在Dao中定义查询用户权限标识
Set findUserPermission(Integer userId);
在Mapper中添加查询用户权限标识的SQL映射
修改ShiroRealm中获取权限并封装权限的信息的方法
//此方法负责获取授权信息
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户id(登录时传入的用户身份是谁)
SysUser user = (SysUser) principalCollection.getPrimaryPrincipal();
//基于登录用户id获取用户权限标识
Set userPermission = sysMenuDao.findUserPermission(user.getId());
//封装数据并返回
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setStringPermissions(userPermission);
return simpleAuthorizationInfo;//返回给授权管理器
}
授权原理分析(底层基于AOP实现):
1、页面上用户通过菜单触发对服务端资源的访问。
2、服务端Controller处理客户端的资源访问请求。
3、假如客户端请求访问的资源业务方法上有@RequiresPermissions注解描述则底层Controller对象会调用Service的代理对象,代理对象会调用AOP中通知方法,在通知方法中获取@RequiresPermissions("sys:user:update")上定义的权限标识。
4.通过Subject对象提交@RequiresPermissions注解中的授权标识给SecuirtyManager对象,此对象会调用ShiroRealm中的获取用户权限的方法,最终会将从数据库取到的权限信息与@RequiresPermissions中定义的权限信息做一个比对
页面呈现登录用户信息
//http://localhost/doIndexUI
@GetMapping("doIndexUI")
public String doIndexUI(Model model) {
//获取登录用户信息(shiro框架给出的固定写法)
SysUser user = (SysUser)SecurityUtils.getSubject().getPrincipal();
model.addAttribute("username", user.getUsername());
return "starter";
}
打开starter.html页面,找到用户名对应位置,然后通过[[${}]]表达式获取服务端model中数据,呈现在页面上
[[${username}]]
用户菜单的动态化呈现(不同用户登录后看到的菜单信息是不同的)
定义pojo对象存储用户菜单信息
package com.cy.pj.sys.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class SysUserMenu implements Serializable {
private static final long serialVersionUID = 3542053307789523530L;
private Integer id;
private String name;
private String url;
private List childMenus;
}
在SysMenuDao中定义查询用户一级和二级菜单信息的方法
ListfindUserMenus(Integer userId);
在SysMenuMapper中定义查询用户一级和二级菜单信息的对应SQL映射
修改PageController中doIndexUI方法
//http://localhost/doIndexUI
@GetMapping("doIndexUI")
public String doIndexUI(Model model) {
//获取登录用户信息(shiro框架给出的固定写法)
SysUser user = (SysUser) SecurityUtils.getSubject().getPrincipal();
model.addAttribute("username", user.getUsername());
List userMenus = sysMenuService.findUserMenus(user.getId());
model.addAttribute("userMneus", userMenus);
return "starter";
}
修改starter页面菜单呈现部分的内容
[[${um.name}]]
Shiro框架中对密码的加密实现:
首先是引入依赖
org.apache.shiro
shiro-spring
1.7.0
1.要拿到一个盐值
String salt = UUID.randomUUID().toString();
产生一个随机字符串作为加密盐使用
2.对密码进行加密
SimpleHash simpleHash = new SimpleHash("MD5",entity.getPassword(),salt,1);
需要传入的参数,从左往右的顺序分别是:加密算法、对谁进行加密、加密使用的盐值是谁、加密次数
3.把加密完的结果改为十六进制
String hashedPassword = simpleHash.toHex();
MD5加密算法的特点:
1.不可逆;
2.相同内容加密结果也相同,将密码存储到数据库的同时也会存储对应的盐值,存储盐值就是为了以后登录的时候再拿出来对比;例如:第一次设置完密码,加密完了的密文;下次登录的时候,会把登录时输入的密码再次加密,然后对比两个加密后的密文值,如果是一致的说明密码一致。
@Test
void testMD502() {
String password="12345";
String salt= UUID.randomUUID().toString();
System.out.println(salt);//d5a3cfb5-b952-40cc-af62-fe79c76ad157
SimpleHash simpleHash=new SimpleHash("MD5",password,salt,1);
System.out.println(simpleHash.toHex());//9f23b14e61787294cf4dc5934e04f594
}
@Test
void testMD503() {
String password="12345";
// String salt= UUID.randomUUID().toString();
String salt="d5a3cfb5-b952-40cc-af62-fe79c76ad157";
SimpleHash simpleHash=new SimpleHash("MD5",password,salt,1);
System.out.println(simpleHash.toHex());//9f23b14e61787294cf4dc5934e04f594
}
为什么要加盐值,是因为随机的字符串和密码放一起加密的安全性更高。