基于shiro的通用鉴权组件Jarvis
大部分应用都有鉴权的需求。如应用认证,要求访问者需要登录才能访问。再如应用授权,不仅要求访问者登录,还需要访问被授予权限才能访问特定资源。
为每个大大小小应用开发安全模块其实是一件繁琐的事情。每个应用的安全模块都提供认证和授权功能。
jarvis是一个通用的spring java应用鉴权组件,具有如下特性:
- 组件化。需要配置的项目仅需引用组件的maven依赖,提供简单配置文件即可拥有鉴权模块...
- 在线化。提供在线web配置,在线管理用户,角色,权限。
- 实时化。实时维护资源权限,实时生效(通常项目的url资源访问权限由配置文件进行定义,如果修改需要重启项目)。
- url权限粒度
Apache Shiro
什么是shiro?
日语发音,中文意思是“城堡”,是一个功能强大且容易使用的java安全框架。提供认证,授权,加密,会话管理等功能,被广泛使用。
Servlet过滤器
Filter是在Servlet 2.3之后增加的新功能,当需要限制用户访问某些资源或者在处理请求时提前处理某些资源的时候,就可以使用过滤器完成。
shiroFilter
Shiro对Servlet容器的FilterChain进行了代理,先执行Shiro自己的Filter链,再执行Servlet容器的Filter链(即原始的Filter)。
shiro 核心概念
Subject、SecurityManager、Realm
Subject
表示当前正在执行操作的用户,用户通常是人,也可以是其他应用进程。获取当前用户的代码很简单:
import org.apache.shiro.subject.Subject;
import org.apache.shiro.SecurityUtils;
...
Subject currentUser = SecurityUtils.getSubject();
一旦获取到当前用户实例对象,就可以进行登录登出,权限检查等操作...
SecurityManager
Subject代表当前用户,SecurityManager代表所有用户,是shiro框架的核心。
当Subject进行login或logout或checkPermission时,其实都是交由SecurityManager进行,SecurityManager能协调这些组件完成工作。
- Authenticator 负责登录验证。获取安全数据(通过realm),获取当前Subject的信息,两者比较,通过则登录成功,失败则抛出异常。
- Authorizer 负责登录Subject的权限校验。
- SessionManager 创建,维护,清除所有应用session。
- CacheManager 缓存管理组件,shiro需要可以获取用户安全数据用于认证,通常这些数据不会经常变动,对这些数据做缓存可以提升框架性能。
- Cryptography 加密组件,如密码加密。
通常开发者很少直接和SecurityManager进行交互...SecurityManager采用门面模式(外观模式),可以当成一个容器类,包含各个安全组件,及各个组件的get,set方法,因此也可以看成是一个java bean,这种结构的类可以很方便的进行xml配置...扩展性也强,可以配置自己实现的组件。SecurityManager不进行真正的Security操作,但知道何时进行并调用协调各个组件进行工作。
- Realms
Realm
realm是Shiro和应用安全数据的“桥梁”。进行登录或访问控制的时候,shiro需要获取应用的用户,角色,权限等数据,shiro通过配置好的realm来获取这些数据,从这点来看,realm是一个DAO
(数据访问对象)。shiro提供了一些内置的realm组件,用于访问不同数据源的安全数据,如关系型数据库,文本配置文件等。开发者也可以实现自己的Realm,如果这些内置的realm不满足需求。
Jarvis基于Apache Shiro的登录流程
- controller接口,接收登录请求,请求参数通常包含用户名和密码,封装成shiro的 UsernamePasswordToken对象,获取当前Subject,调用login(token)方法;,将用户信息提交给认证authenticator组件。
@RequestMapping(value = "/login")
public String login(@Valid User user, BindingResult bindingResult, HttpServletRequest request, Map map) {
if (bindingResult.hasErrors()) {
return "index";
}
String username = user.getUsername();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword(), false);
// 获取当前的Subject
Subject currentUser = SecurityUtils.getSubject();
try {
logger.info("对用户[" + username + "]进行登录验证..验证开始");
currentUser.login(token);
logger.info("对用户[" + username + "]进行登录验证..验证通过");
} catch (UnknownAccountException uae) {
logger.info("对用户[" + username + "]进行登录验证..验证未通过,未知账户");
.....
.....
- authenticator组件调用自定义的realm组件doGetAuthenticationInfo(AuthenticationToken token)方法。
其中doGetAuthenticationInfo(AuthenticationToken token)方法是登录验证的安全数据获取方法,代码逻辑如下:
- 根据当前Subject提供的用户名称查询出用户
- 用户为null,返回null,subject.login(token)方法抛未知账户异常
- 如果用户不为null,封装成SimpleAuthenticationInfo实体对象并返回,shiro的Authentication组件会进行密码校验,校验失败则抛出异常!(校验前按需进行密码解密,这些shiro都会帮开发者完成)。
以下是realm组件doGetAuthenticationInfo(用户数据获取)的实现
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
// UsernamePasswordToken对象用来存放提交的登录信息
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
logger.info("验证当前Subject时获取到token为:" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE));
// 查出是否有此用户
User user = userService.findByName(token.getUsername());
if (user != null) {
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user.getUsername(), // account
user.getPassword(), // 密码
ByteSource.Util.bytes(user.getCredentialsSalt()), // username+salt
getName() // realm name
);
// 若存在,将此用户存放到登录认证info中,无需自己做密码对比,Shiro会为我们进行密码对比校验
return info;
}
return null;
}
- 处理登录校验结果。shiro的authentication组件根据校验产生的错误会抛出各种异常。
try {
logger.info("对用户[" + username + "]进行登录验证..验证开始");
currentUser.login(token);
logger.info("对用户[" + username + "]进行登录验证..验证通过");
} catch (UnknownAccountException uae) {
logger.info("对用户[" + username + "]进行登录验证..验证未通过,未知账户");
map.put("message", "未知账户");
} catch (IncorrectCredentialsException ice) {
logger.info("对用户[" + username + "]进行登录验证..验证未通过,错误的凭证");
map.put("message", "密码不正确");
} catch (LockedAccountException lae) {
logger.info("对用户[" + username + "]进行登录验证..验证未通过,账户已锁定");
map.put("message", "账户已锁定");
} catch (ExcessiveAttemptsException eae) {
logger.info("对用户[" + username + "]进行登录验证..验证未通过,错误次数过多");
map.put("message", "用户名或密码错误次数过多");
} catch (AuthenticationException ae) {
// 通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景
logger.info("对用户[" + username + "]进行登录验证..验证未通过,堆栈轨迹如下");
ae.printStackTrace();
map.put("message", "用户名或密码不正确");
}
shiro权限配置
shiro权限配置一般是这样的:
告诉shiro哪些资源需要哪些角色或哪些权限...
<property name="filterChainDefinitions" >
<value>
/** = anon
/page/login.jsp = anon
/page/register/* = anon
/page/index.jsp = authc
/page/addItem* = authc,roles[数据管理员]
/page/file* = authc,roles[普通用户]
/page/listItems* = authc,perms[删除数据]
/page/showItem* = authc,perms[添加数据]
/page/updateItem*=authc,roles[数据管理员]
</value>
</property>
当请求传递到PathMatchingFilter过滤器时,PathMatchingFilter根据请求url路径解析对应的配置(anon,authc),即mappedValue,然后调用onPreHandle(),进行权限校验,和realm提供的安全数据比对,检查当前用户是否拥有mappedValue所要求的权限。
boolean pathsMatch(String path, ServletRequest request)
boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception
动态url权限配置
大部分shiro应用使用配置文件配置资源权限,修改配置后需要重启才能生效。
AbstractShiroFilter abstractShiroFilter = null;
try {
abstractShiroFilter = (AbstractShiroFilter)bean.getObject();
} catch (Exception e) {
e.printStackTrace();
}
// 获取过滤管理器
PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) abstractShiroFilter
.getFilterChainResolver();
DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();
// 清空初始权限配置
manager.getFilterChains().clear();
bean.getFilterChainDefinitionMap().clear();
// 重新构建生成
set(bean,definitions);
Map<String, String> chains = bean.getFilterChainDefinitionMap();
for (Map.Entry<String, String> entry : chains.entrySet()) {
String url = entry.getKey();
String chainDefinition = entry.getValue().trim().replace(" ", "");
manager.createChain(url, chainDefinition);
}
Jarvis基于Apache Shiro的权限控制流程
Jarvis采用用户-角色-权限的权限控制体系。
表结构设计:
user用户表
role角色表
permission权限表
用户角色关系表 多对多关系
角色权限关系表 多对多关系
谈谈角色
角色是权限的集合。
隐式角色和显式角色(implicit role和explicit role)
- 隐式角色。使用隐式角色做权限校验并不关心权限,仅仅关心角色本身。即能做什么事情,是因为拥有什么角色...
- 显式角色。 能做什么事情,是因为拥有什么角色,而这个角色拥有这个权限去做这件事。
Jarvis即支持隐式角色也支持显式角色,以下是realm组件doGetAuthorizationInfo(权限安全数据获取)的实现:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.info("##################执行Shiro权限认证##################");
String loginName = (String) super.getAvailablePrincipal(principalCollection);
// 到数据库查是否有此对象
User user = userService.findByName(loginName);// 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
if (user != null) {
// 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 用户的角色集合
info.setRoles(user.getRolesName());
// 用户的角色对应的所有权限,如果只使用角色定义访问权限,下面的四行可以不要 隐式角色
List<Role> roleList = user.getRoleList();
for (Role role : roleList) {
info.addStringPermissions(role.getPermissionsName());
}
return info;
}
// 返回null的话,就会导致任何用户访问被拦截的请求时,都会自动跳转到unauthorizedUrl指定的地址
return null;
}
Jarvis集成超级详情日志分析应用
引入maven依赖
<dependency>
<groupId>com.hlg.bigdata</groupId>
<artifactId>Jarvis</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
配置文件application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/cjxqlog?useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=5581653
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#logging.path=/Users/yangwq/cjxq/
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
spring.jpa.properties.hibernate.hbm2ddl.auto=update
未来可以考虑集成data-plant,商品分类系统等需要鉴权的应用。