Shiro介绍
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。毕竟官网有十分钟入门,足矣可见该框架的上手难度。
Shiro 功能介绍
从上图可看出来,主要功能有4个,以及支持一些其他特性。
主要功能
Authentication:认证,用于登录
Authorization:授权,判断用户是否拥有资源权限
Session Management:会话,登录后就是一次会话。
Cryptography:加密,用户密码密文存入数据库。
支持的功能
Web Support:支持Web,SE也能用
Caching:缓存
Concurrency:支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去
Testing:提供测试支持
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问
Remember Me:记住我,
PS: Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro
在使用的时候还有一个很重要的对象叫:Subject ,该对象表示当前访问的用户。
Shiro的一些配置
- 自定义Realm,需继承AuthorizingRealm。用户认证与授权。
- 把Realm交给SecurityManager。统一管理。
- 配置 ShiroFilterFactoryBean 把需要拦截的地址和SecurityManager
第一步:写一个Realm,来认证。
AuthorizingRealm 该接口需要重写两个方法。
doGetAuthorizationInfo 授权,访问某一个具体资源判断是否有该权限
doGetAuthenticationInfo 认证,登录认证
public class UserRealm extends AuthorizingRealm {
@Resource
private UserInfoService userInfoService;
@Resource
private MenuMapper menuMapper;
/**
* 认证,你需要通过用户名查数据库,在进行密码对比(内部自动对比,错误会抛异常)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// token 是在Controller层传入进来的
// getPrincipal 内部代码调用的就是 getUsername()方法
String username = (String)token.getPrincipal();
// 通过用户名查询该用户信息
UserInfo user = userInfoService.login(username);
if(user != null){
// 第一个参数存用户信息。这里存的,其他地方可以取出来。存什么,则取什么。
// 第二个参数查询出来的密码,内部会判断该密码和输入密码是否相同
// 第三个参数自定义Realm的信息
return new SimpleAuthenticationInfo(user, user.getPwd(), getName());
}
return null;
}
/**
* 授权,写了一个授权判断的时候会自动执行以下代码。
* 注意如果授权你采用注解的方式,必须在配置中添加两个Bean,否则不会进入该方法判断。
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 该方法就是取的认证时,存的第一个参数。
UserInfo user = (UserInfo) principals.getPrimaryPrincipal();
if(user == null){
return null;
}
List
第二部写一个配置类把自定义Realm交给SecurityManager
// 申明配置类
@Configuration
public class ShiroConfig {
// 把自定义存入ioc容器中
@Bean
public UserRealm initRealm() {
return new UserRealm();
}
@Bean
public SecurityManager initSecurityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 将自定义Realm注入进去
manager.setRealm(initRealm());
// 这个是记住我功能
manager.setRememberMeManager(cookieRememberMeManager());
return manager;
}
@Bean
public CookieRememberMeManager cookieRememberMeManager() {
//实例化rememberme管理器
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
//定义Cookie cookie的名字为rememberMe
SimpleCookie cookie = new SimpleCookie("rememberMe");
//定义Cookie的有效时间(s)
cookie.setMaxAge(24 * 60 * 60 * 3);
//将cookie设置到rememberme管理器中
cookieRememberMeManager.setCookie(cookie);
//设置cookie的值的加密密钥(设置用户数据序列化以后采用的加密密钥)
cookieRememberMeManager.setCipherKey(Base64.decode("6ZmI6I2j5Y+R5aSn5ZOlAA=="));
return cookieRememberMeManager;
}
/**
* 该方法用于过滤请求
*/
@Bean
public ShiroFilterFactoryBean shiroFilter() throws UnsupportedEncodingException {
//实例化Filter工厂
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//注册securityManager
shiroFilterFactoryBean.setSecurityManager(initSecurityManager());
//设置Shiro过滤器过滤规则
//LinkHashMap是有序的,shiro会根据添加的顺序进行拦截,匹配到过滤器后就执行该过滤器不会在继续向下查找过滤器
Map filterChainDefinitionMap = new LinkedHashMap();
/*
* anon:所有的url都可以不登陆的情况下访问
* authc:所有url都必须 认证 通过才可以访问
* user:有记住我才能访问
* role:拥有某个角色才能访问 filterChainDefinitionMap.put("/user/add", "perms[user:add]");
* perms:拥有某个资源的权限才能访问
*/
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/login.html", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/**", "user");
//未登录时重定向的网页地址
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 未授权的时候跳转地址
shiroFilterFactoryBean.setUnauthorizedUrl("/login.html");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
// 这下面两个 Bean 用于开启注解形式的授权验证。(就是开启AOP)
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(initSecurityManager());
return advisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator app=new DefaultAdvisorAutoProxyCreator();
app.setProxyTargetClass(true);
return app;
}
}
这个配置的意思是:放行静态资源,登录页面,和登录请求,其他全部请求都被拦截需要对应的权限。
以上就是Shiro 相关的配置就算是写完了。但是是不够的,因为没有调用上面的方法。
@RequestMapping("/login")
public Result login(String username,String pwd,boolean rememberme){
// 创建一个token,这里的token 就是自定义Realm取出来的token
UsernamePasswordToken token = new UsernamePasswordToken(username, pwd, rememberme);
// subject 表示当前访问的用户
Subject subject = SecurityUtils.getSubject();
// 判断是否认证过 认证过为true,没认证为false
if (!subject.isAuthenticated()){
// 执行登录方法,该方法不是Shiro提供的,但是最终会执行自定义Realm的认证方法上面。
subject.login(token);
}
return new Result("200","ok",null,null);
}
这样一个认证的流程就算走完了。
- 自定义Realm
- 编写配置类,把Realm注入IOC容器,并且放进SecurityManager
- 编写请求过滤,并把SecurityManager放入ShiroFilterFactoryBean对象中
- 在登录方法中,把用户名、密码和记住我封装进UsernamePasswordToken对象中。
- 使用SecurityUtils获取Subject。
- 通过Subject.login(token)进行验证,验证成功则正常执行,验证失败则抛异常。
代码逻辑对比
在没有Shiro的时候代码逻辑是这样的
Controller接受用户名密码,调用Service,查询dao层,结果返回给Service层,Service层进行密码对比并把结果返回给Controller。
有了Shiro是这样的:
Controller接受用户名密码,对用户名密码进行一次封装,接着调用subject的login方法。
login方法,会去自动找到写的Realm(前提继承了AuthorizingRealm),在Realm调用Service,dao层查询,结果层层返回,返回到Realm,对结果集进行再次封装。封装的里面他会自动进行密码的对比。失败则抛异常,成功则返回到Controller层。
图形化说明:
[图片上传失败...(image-a93460-1593264897452)]
授权方式
编码方式授权判断:
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("administrator")) {
//拥有角色administrator
} else {
//没有角色处理
}
if(subject.isPermitted("insert")){
// 拥有某权限
}else{
// 没有权限
}
注解方式:(配置文件需要额外配置,上面有说明)
注解 | 意义 | 案例 |
---|---|---|
@RequiresAuthentication | 验证用户是否登录 | |
@RequiresUser | 当前用户已经验证过了或则记住我了 | |
@RequiresGuest | 是否是游客身份 | |
@RequiresRoles | 拥有该角色 | @RequiresRoles({“admin”}) |
@RequiresPermissions | 需要拥有权限 | @RequiresPermissions("/development/bug/update") |
密码加密
上面的例子当中并没有涉及到密码加密,但是流程都是一毛一样的,唯一区别就是密码不同。
new SimpleHash(algorithmName, source, salt, hashIterations) 用来加密
new SimpleHash("MD5", "123456", "abcd")
四个参数:
algorithmName 加密方式写 MD5
source 需加密的密码
salt 加密的盐
hashIterations 加密几次
在自定义Realm里面同样调用 new SimpleAuthenticationInfo方法,只不过参数变成了4个
第一个参数存用户信息。这里存的,其他地方可以取出来。存什么,则取什么
第二个参数查询出来的密码,内部会判断该密码和输入密码是否相同
第三个参数 加密盐,需要通过 ByteSource.Util.bytes(solt)
第四个参数自定义Realm的信息
but !!! 虽然你这样写了。但是!他内部是不会加密滴!
需要配置文件中加入这么一个东东:
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法, 与注册时使用的散列算法相同
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
// 散列次数, 与注册时使用的散列册数相同
hashedCredentialsMatcher.setHashIterations(1);
// 生成16进制 默认生成的32位
//hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
// 这么做了还差一点点,要把这个Bean注入 自定义Realm中
@Bean
public UserRealm initRealm() {
UserRealm userRealm = new UserRealm();
// 注入进去它才会使用,这下就没问题了
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return userRealm;
}
最后补充一点
你会发现一个问题,那就是每次授权的时候他都会走查询,意味着每次都要查询数据库,如果访问量大了明显很难受的,所以可以加缓存!
使用Ehcache(系统混合缓存方案);
使用本地内存缓存方案;
自定义CacheManager(比如Redis用来作为缓存)
这里就短暂介绍一下最简单的(方案二)。
很简单,就算配置成Java代码也是分分钟的事情,这样只会第一次走查询,后面都走缓存了
但是还是有缺点:我在你登录期间修改了你的权限,并不能动态修改。他依旧走的是缓存。
所以其他方案以后在整一整。