shiro是什么?
shiro是一个权限管理框架。什么是权限管理呢?
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
这么官方的说法其实我感觉并不好懂,换一个通俗一点的理解:
现在系统有三个模块,A,B,C。对于每个用户讲,可能有的能用A,有的能用B,有的能用C。当然也可以能用AC,BC,ABC。
但是如果是在菜单中把A,B,C都展示出来,然后没权限的点击显示无此权限。先不说这样操作要每个接口都判断增加了多少工作量,就说既然没权限还让用户看到就挺不友好的,所以这里最好的解决办法就是用户没权限就不显示相应的菜单好了。
还有时候,一个公司的办公软件,财务可以看到账单什么的,客服可以看到一些通知投诉消息,老板可以随意查看员工信息包括办公室监控,hr可以查看kda,开发可以提交bug(开玩笑),这样每个人都有自己独立的功能。但是肯定一个公司就一个办公系统,这个时候就需要很细化每个人拥有的权限啦。而且其实一个员工在入职的时候就决定了岗位,也就是所谓的“角色”。最好的办法自然就是创建这个员工的系统账号的时候就直接分配好这个员工拥有的访问权限啦。
而shiro就是这样一个用来做权限管理的框架。
shiro是怎么实现的?
代码的世界,没有无缘无故的爱也没无缘无故的恨,既然能实现肯定是有原因滴。
shiro 是怎么实现的权限管理呢?
答案是依据数据库。
shiro一般需要5张表:3张实体表user,role,menu(一对一),2张关系映射表user-role,role-menu(一对多)
这五张表的大体关系我用一张图来说明:
上面的好多形容词和比喻都是我觉得比较容易懂的,可能有的说的不严谨,用来理解就行,千万别当成正确答案。
然后大体关系就是这样的。至于表结构,我截图来展示下:
菜单表
这个表中的name就是前端左侧导航栏的名字,url是前端页面的路径。
角色表
我这里的数据都是自己用来测试的,看表结构就行了。
角色对应菜单表
用户对应角色表
这两个单纯的关联表。
最后的用户表我就不贴出来了,虽说尽量少的业务逻辑,但是随着做必然会加上好多无用的字段,反正必要的id,账号,密码,盐,手机号,父账号(看情况决定要不要),状态等等,反正就是自己用着舒服方便就行了。
然后其实shiro实现权限控制真正靠的就是这五张表维护关系。
怎么用表的关联关系权限的?
首先,不管什么用户,第一步肯定是登录。登陆之后才会进入菜单页。
这里不谈业务逻辑也不谈验证码密码加密解密啥的。就当是这个用户登录成功了。然后应该做的是什么呢?
- 确定当前用户id
- 用该id去用户-角色表查询,查询出一个list(最少一个,最多不知道了)
- 用这些角色的id去查询角色-菜单表,又查询出一堆菜单记录。
- 把这些菜单记录传给前端,前端就知道要怎么在导航栏展示出这些菜单了。
不要害怕2,3这两步查询,因为其实真正项目中这两个表不会有多少条记录。多大个项目能有几百个角色啊?能有上千个菜单?纯扯淡。
大概怎么分用户/身份,让他们看到该看到的,看不到不该看到的粗略的逻辑就是这样。其实公司系统很多是在创新新员工的同时就可以分配角色/权限了。甚至最高权限还可以增加角色,删除角色之类的,反正操作的都是这几张关系表。
token认证
刚刚说的是让用户只能看到想让他看到的,而token则是让我们知道这个用户是谁。
在shiro中token 是要自己生成的。下面是我生成token 的工具类(我是把token保存在数据库了,所以会有相应的代码。现在其实token也可以存缓存什么的,酌情使用吧)
这个是token生成器(自定义的)
public class TokenGenerator {
public static String generateValue() {
return generateValue(UUID.randomUUID().toString());
}
private static final char[] hexCode = "0123456789abcdef".toCharArray();
public static String toHexString(byte[] data) {
if(data == null) {
return null;
}
StringBuilder r = new StringBuilder(data.length*2);
for ( byte b : data) {
r.append(hexCode[(b >> 4) & 0xF]);
r.append(hexCode[(b & 0xF)]);
}
return r.toString();
}
######这个是具体生成token的方法,
public static String generateValue(String param) {
try {
MessageDigest algorithm = MessageDigest.getInstance("MD5");
algorithm.reset();
algorithm.update(param.getBytes());
byte[] messageDigest = algorithm.digest();
return toHexString(messageDigest);
} catch (Exception e) {
throw new RRException("生成Token失败", e);
}
}
}
public R createToken(long userId) {
//生成一个token
String token = TokenGenerator.generateValue();
//当前时间
Date now = new Date();
//过期时间
Date expireTime = new Date(now.getTime() + EXPIRE * 1000);
//判断是否生成过token
SysUserTokenEntity tokenEntity = this.getById(userId);
if(tokenEntity == null){
tokenEntity = new SysUserTokenEntity();
tokenEntity.setUserId(userId);
tokenEntity.setToken(token);
tokenEntity.setUpdateTime(now);
tokenEntity.setExpireTime(expireTime);
//保存token
this.save(tokenEntity);
}else{
tokenEntity.setToken(token);
tokenEntity.setUpdateTime(now);
tokenEntity.setExpireTime(expireTime);
//更新token
this.updateById(tokenEntity);
}
return R.ok().put("token", token).put("expire", EXPIRE);
}
然后如果登陆成功的话,我是直接把这个返回值返回给前端了。以后的每次访问我都会先验证token。
创建一个过滤器,每次访问都要验证token:
public class OAuth2Filter extends AuthenticatingFilter {
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if(StringUtils.isBlank(token)){
return null;
}
return new OAuth2Token(token);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){
return true;
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token,如果token不存在,直接返回401
String token = getRequestToken((HttpServletRequest) request);
if(StringUtils.isBlank(token)){
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));
httpResponse.getWriter().print(json);
return false;
}
return executeLogin(request, response);
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());
String json = new Gson().toJson(r);
httpResponse.getWriter().print(json);
} catch (IOException e1) {
}
return false;
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest){
//从header中获取token
String token = httpRequest.getHeader("token");
//如果header中不存在token,则从参数中获取token
if(StringUtils.isBlank(token)){
token = httpRequest.getParameter("token");
}
return token;
}
}
(我记得当时这个好像是在网上找的,修修改改了下?我顺便把几个工具类都贴一下)
public class OAuth2Token implements AuthenticationToken {
private String token;
public OAuth2Token(String token){
this.token = token;
}
@Override
public String getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
授权认证:
@Component
public class OAuth2Realm extends AuthorizingRealm {
@Autowired
private ShiroService shiroService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token;
}
/**
* 授权(验证权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUserEntity user = (SysUserEntity)principals.getPrimaryPrincipal();
Long userId = user.getUserId();
//用户权限列表
Set<String> permsSet = shiroService.getUserPermissions(userId);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet);
return info;
}
/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String accessToken = (String) token.getPrincipal();
//根据accessToken,查询用户信息
SysUserTokenEntity tokenEntity = shiroService.queryByToken(accessToken);
//token失效
if(tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()){
throw new IncorrectCredentialsException("token失效,请重新登录");
}
//查询用户信息
SysUserEntity user = shiroService.queryUser(tokenEntity.getUserId());
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
return info;
}
}
咳咳,回归正题,创建完过滤器了还要使用啊,这个是在shiro 的配置中配置的:
/**
* Shiro配置
*
* @author Mark sunlightcs@gmail.com
*/
@Configuration
public class ShiroConfig {
@Bean("securityManager")
public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(oAuth2Realm);
securityManager.setRememberMeManager(null);
return securityManager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//oauth过滤
Map<String, Filter> filters = new HashMap<>();
filters.put("oauth2", new OAuth2Filter());
shiroFilter.setFilters(filters);
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/sys/login", "anon");
filterMap.put("/token", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/imgs/**", "anon");
filterMap.put("/img2s/**", "anon");
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
其中有些接口不能拦截,比如登录,这个时候肯定没token呢,如果给拦截了那还怎么继续了?所以在这也可以把不用拦截的接口列出来。到这基本上就完成了,从登录验证,到接口拦截,到权限管理。
另外我这还有一个shiro封装的工具类,这样平时调用比较方便。
/**
* Shiro工具类
*
* @author Mark sunlightcs@gmail.com
*/
public class ShiroUtils {
public static Session getSession() {
return SecurityUtils.getSubject().getSession();
}
public static Subject getSubject() {
return SecurityUtils.getSubject();
}
public static SysUserEntity getUserEntity() {
return (SysUserEntity)SecurityUtils.getSubject().getPrincipal();
}
public static Long getUserId() {
return getUserEntity().getUserId();
}
public static void setSessionAttribute(Object key, Object value) {
getSession().setAttribute(key, value);
}
public static Object getSessionAttribute(Object key) {
return getSession().getAttribute(key);
}
public static boolean isLogin() {
return SecurityUtils.getSubject().getPrincipal() != null;
}
public static String getKaptcha(String key) {
Object kaptcha = getSessionAttribute(key);
if(kaptcha == null){
throw new RRException("验证码已失效");
}
getSession().removeAttribute(key);
return kaptcha.toString();
}
}
对了,上面的代码是我用人人框架写的,这个错误RRException也是自己封装的一个异常。剩下也不知道说啥了,就这样吧。随时想到再补充。
另外附上几个我觉得不错的技术贴:
Spring Boot整合Shiro
springboot+shiro+redis项目整合
然后这个笔记就到这里了,如果稍微帮到你了记得点个喜欢点个关注,另外我这是想到哪说到哪,如果有什么遗漏或者不清楚的可以留言或者私信问我。也祝大家工作顺顺利利!生活健健康康~~~!
网友评论