Java权限控制框架看它就够了共分为三个部分:
1、Java权限控制框架看它就够了之Shiro
2、Java权限控制框架看它就够了之Spring Security
3、Java权限控制框架看它就够了之Spring Security Oauth2
这三个权限框架博客将于近期发布,敬请关注哦!!!!!!!!!!!!!!!!!!!!!!!!!!!!
一、Shiro简介
首先我还是先介绍一波Shiro是做什么的吧。Shiro框架是Apache基金会开源维护的一个Java安全框架。 它为开发人员提供一个直观而全面的认证,授权,加密及会话管理的解决方案,同时由于它使用起来小巧、方便,因此深受很多开发人员的青睐,不过在我看过的代码中基本都是在管理系统中使用的比较多,而且Shiro提供了jsp权限控制的标签,相对于前后端分离项目中个人认为它与后续要说的SpringSecurity在某些功能上是无异的,具体的优缺点会在下一次的博客中进行说明。
二、Shiro能做什么?
- Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
- Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
- Web Support:Web支持,可以非常容易的集成到Web环境;
- Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
- Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
- Testing:提供测试支持;
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
说了这么多,其实就是对用户所访问的资源进行相关的权限控制,让系统知道是谁登录了系统、登陆者有哪些权限,是否能访问对应资源。
下面我从Shiro的认证和授权主要的方面来说明认证和授权的流程以及如何在应用中实现Shiro的权限控制。
三、认证的基本流程
在说明认证流程之前需要了解几个名词:
- Subject:主体,代表了当前“用户”,我们可以认为Subject是Shiro认证的开始,即对外暴露的API,但是实际上我们的认证操作并不是有Subject来完成的,而是由Subject提交到SecurityManager,后续所有的操作均是由其完成的,也就是说SecurityManager才是实际的执行者;
- SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互;
- Realm:域,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作。
根据以上的描述,下面就此图进行详细说明:
1、在项目启动时,系统会加载SecurityManger的Bean,创建Security Manager的环境供后续的操作使用;
2、当用户输入用户名和密码请求登录时,系统根据用户名判断用户是否存在以及密码是否正确,随后使用用户信息生成token,将用户信息提交Security Manager进行认证操作
3、 SecurityManager得到token信息后,通过调用authenticator.authenticate(token)方法,把身份验证委托给内置的Authenticator的实例进行验证。authenticator通常是ModularRealmAuthenticator
4、ModularRealmAuthenticator调用Realms去查询用户信息,进行一系列的认证操作。
总结来说,应用通过Subject来进行认证和授权,而Subject又委托给SecurityManager;SecurityManager根据注入的Realm,使其能得到合法的用户及其权限进行判断。这样就完成了认证的操作。
四、授权的基本流程
由于我们是通过注解的方式进行是否授权的判断,因此我们通过IDEA下载shiro的相关源码,并进入@RequiresPermissions("test")的注解,通过注释我们可以了解到添加此注解会被拦截到具体的类和方法中,通过提示我们进入org.apache.shiro.subject.Subject#checkPermission,可以发现checkPermission的实现类对应的是DelegatingSubject类(或子类)的实例对象。
在认证开始时,通过SecurityManager实例来调用securityManager.checkPermission(string)方法, SecurityManager委托Authorizer的实例(默认是ModularRealmAuthorizer类的实例,同样支持多个realm)调用相应的授权方法每一个Realm将检查是否实现了相同的Authorizer接口,然后调用Realm自己的相应的授权验证方法完成本次的授权验证。
五、Shiro在具体应用中的使用(以SpringBoot集成Shiro为例)
1、首先在SpringBoot中引入Shiro相关的依赖,由于以接口方式进行操作,因此引入jwt依赖;
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.1</version>
<scope>compile</scope>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro-spring.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
2、对Shiro相关内容进行配置,包括shiro拦截路径的配置,SecurityManager的配置以及开启注解的配置等;
/**
*
* Shiro配置
* @author liuyh
*
**/
@Slf4j
@Configuration
public class ShiroConfig {
private String[] ignorUrls;
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
this.init();
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
//拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 配置不会拦截的链接,顺序判断
for (String path : ignorUrls) {
filterChainDefinitionMap.put(path, "anon");
}
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<>(1);
filterMap.put("jwt", new JwtFilter());
factoryBean.setFilters(filterMap);
// 过滤链接定义,从上向下顺序执行,一般将/**放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
//未授权界面;
factoryBean.setUnauthorizedUrl("/403");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}
@Bean("securityManager")
public SecurityManager securityManager(ShiroRealm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(evaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 下面的代码是添加注解支持
*
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
private void init() {
try (InputStream input = getClass().getClassLoader().getResourceAsStream("web-pattern.yml")) {
YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
yaml.setResources(new InputStreamResource(input));
ignorUrls = yaml.getObject().values().toArray(new String[0]);
} catch (Exception e) {
log.error("parse web-pattern.yaml error", e);
}
}
}
3、继承AuthorizingRealm,并对其中的doGetAuthenticationInfo和doGetAuthorizationInfo的抽象方法进行定制化实现,以实现用户的认证和授权的功能;
/**
* Shiro的认证和授权
* @author liuyh
**/
@Slf4j
@Component
@SuppressWarnings(value = "all")
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserDao userDao;
@Autowired
private ResourceDao resourceDao;
@Autowired
private UserResourceDao userResourceDao;
@Autowired
private RedisUtil redisUtil;
/**
* 必须重写此方法,不然shiro会报错
*
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可
*
* @param auth
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
try {
String token = auth.getPrincipal().toString();
String username = JwtUtil.getUserName(token);
SysUser user = null;
if (redisUtil.get(CommonConstant.REDIS_USER + username) != null) {
user = JSONObject.parseObject((redisUtil.get(CommonConstant.REDIS_USER + username)).toString(),SysUser.class);
if (user == null) {
user = userDao.findByUsername(username);
}
}else {
user = userDao.findByUsername(username);
}
if (user == null) {
throw new SysException("用户不存在");
}
String cacheToken = String.valueOf(redisUtil.get(CommonConstant.REDIS_TOKEN + token));
if (StringUtils.isNotBlank(cacheToken)) {
if (JwtUtil.verify(cacheToken, username,user.getId())) {
redisUtil.set(CommonConstant.REDIS_USER + username, JSON.toJSON(user).toString(),CommonConstant.EXPIRE_TIME);
redisUtil.expire(CommonConstant.REDIS_TOKEN + token,CommonConstant.EXPIRE_TIME_REDIS);
}else {
throw new SysException("Token校验失败,无法访问!");
}
}else {
throw new SysException("登录过期,请重新登录");
}
return new SimpleAuthenticationInfo(token, token, getName());
} catch (TokenExpiredException e) {
log.error(e.getMessage());
throw new SysException("登录过期");
} catch (JWTVerificationException e) {
log.error(e.getMessage());
throw new AuthenticationException("认证失败");
} catch (Exception e) {
e.printStackTrace();
throw new SysException("系统异常");
}
}
/**
* 只有当需要检测用户权限的时候才会调用此方法
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
String userId = JwtUtil.getUserId(principals.toString());
if (StringUtils.isNotBlank(userId)) {
List<SysUserResource> userResourceList = userResourceDao.findByUserId(userId);
if (userResourceList != null && !userResourceList.isEmpty()) {
List<SysResource> resourceList = resourceDao.findByIdIn(userResourceList.stream().map(SysUserResource::getResourceId).collect(Collectors.toList()));
if (resourceList != null && !resourceList.isEmpty()) {
resourceList.parallelStream().forEach(auth -> info.addStringPermission(auth.getCode()));
}
}
}
return info;
}
}
4、由于使用前后端分离的方式,我们需要拦截请求的地址及其携带的token来进行判断,是否可以放行,因此此处我们需要继承BasicHttpAuthenticationFilter类(通过查看BasicHttpAuthenticationFilter的类我们可以发现,其逐层继承OncePerRequestFilter, OncePerRequestFilter的字面意思是:Once Per Request,即每个请求只执行一次)来进行token的验证
/**
* shiro过滤器
*
* @author LIUYH
**/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
return executeLogin(request, response);
} catch (Exception e) {
log.error("无权限访问,请重新登录");
HttpServletResponse resp = (HttpServletResponse) response;
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = null;
try {
out = resp.getWriter();
out.write(JSON.toJSONString(SysResp.status(SysResp.RspStatus.RELOGIN).build()));
out.flush();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
IOUtils.closeQuietly(out);
}
return false;
}
}
/**
* 登录验证
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Authorization");
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 对跨域提供支持
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
5、至此Shiro的基本核心功能已经完成,我们开始编写登录请求
/**
* 登录业务逻辑实现
*
* @param loginDto 登陆信息
* @return
*/
@Override
public LoginRespDTO login(LoginDTO loginDto) {
try {
SysUser user = userDao.findByUsername(loginDto.getUsername());
if (user != null) {
Subject subject = SecurityUtils.getSubject();
String token = JwtUtil.sign(user.getId(), user.getUsername(), SecureUtil.md5(loginDto.getPassword()));
redisUtil.set(CommonConstant.REDIS_TOKEN + token,token);
redisUtil.expire(CommonConstant.REDIS_TOKEN + token,CommonConstant.EXPIRE_TIME_REDIS);
JwtToken jwtToken = new JwtToken(token);
//执行认证操作.
subject.login(jwtToken);
return new LoginRespDTO(token,user);
}
log.error("【" + loginDto.getUsername() + "】用户不存在");
throw new SysException("用户不存在!");
} catch (SysException ex) {
log.error("【" + loginDto.getUsername() + "】用户不存在");
throw new SysException(ex.getMessage());
} catch (IncorrectCredentialsException ex) {
log.error("【" + loginDto.getUsername() + "】,【" + loginDto.getPassword() + "】密码错误");
throw new SysException(ex.getMessage());
} catch (AuthenticationException ae) {
log.error("【" + loginDto.getUsername() + "】,【" + loginDto.getPassword() + "】登录认证失败");
throw new SysException("用户名或密码错误!");
} catch (Exception e) {
log.error(e.getMessage());
throw new SysException("服务异常,请联系管理员!");
}
}
6、测试用户登录
7、测试是否能对接口进行访问控制(以test接口为例)
首先不携带token进行访问
携带token进行访问
8、测试对赋权是否有效(用户已被赋予“test”的权限)
首先我们在接口上增加Shiro权限控制注解,值为abc,携带token范文返回结果如下:
将接口权限注解的值设置为test, 携带token范文返回结果如
至此,Shiro的基本执行流程以及SpringBoot集成Shiro的操作已经基本完成,已经可以实现基本的权限控制。但是代码中有很多需要性能优化之处,敬请谅解!若本博客存在任何错误之处欢迎留言指出改正,谢谢!
下一章节:Java权限控制框架看它就够了之Spring Security 欢迎关注!