这里对昨天的shiro项目做个说明,整个项目主要参考的是GitHub的一个项目,他是基于session会话的,有集成redis,如果需要的话大家可以参考下:https://github.com/lovelyCoder/springboot-shiro。
我的项目GitHub地址:https://github.com/rhettpang/Springboot-Shiro。
现在说下我的无状态的shiro,先把项目结构列出来好做说明:
- config:shiro的配置;
- constant:自己用到的常量;
- controller:为测试用的接口:DefaultExceptionHandler这个类专门用来处理shiro抛出的异常,SimpleErrorController捕获项目异常,返回一个友好的json格式;
- filter:自己重写的shiro的filter
- redis:对redis的集成(原打算用的,需求有变化,后来发现暂时用不到了,这里提供出来给大家参考下)
- shiro:shiro的realm和相关的一些实现 util:这里目前只有一个MD5加密工具类,用来对密码加密的
- mapper和service大家都理解
resources中还有些配置,大家自己把代码宕下来看看就好。
1. ShiroConfig
ShiroConfig中的部分代码:
*
Filter Chain定义说明
1、一个URL可以配置多个Filter,使用逗号分隔
2、当设置多个过滤器时,全部验证通过,才视为通过
3、部分过滤器可指定参数,如perms,roles
*
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){
log.info("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/usersPage");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//自定义拦截器
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
filtersMap.put("myAccessControlFilter", new MyAccessControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
//拦截器.
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
//我做的是无状态的,这里的东西实际上是用不到的,仅供参考
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/css/**","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/img/**","anon");
filterChainDefinitionMap.put("/font-awesome/**","anon");
// filterChainDefinitionMap.put("/users", "anon");
filterChainDefinitionMap.put("/createPermission", "anon");
filterChainDefinitionMap.put("/**", "myAccessControlFilter");
// filterChainDefinitionMap.put("/**", "authc");
//<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
//<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
//自定义加载权限资源关系
// List<Resources> resourcesList = resourcesService.queryAll();
// for(Resources resources:resourcesList){
//
// if (StringUtil.isNotEmpty(resources.getResurl())) {
// String permission = "perms[" + resources.getResurl()+ "]";
// filterChainDefinitionMap.put(resources.getResurl(),permission);
// }
// }
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
这里有很多备注,相信大家都能看懂,简单说下其中的重点。
//自定义拦截器
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
filtersMap.put("myAccessControlFilter", new MyAccessControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
- 1
- 2
- 3
- 4
这块内容备注的是自定义拦截器,准确说来,这里应该是加载自定义的拦截器。在我项目的filter包中的MyAccessControlFilter就是在这里加载进来的。必须要说下,这里只能通过new MyAccessControlFilter()这种形式加载,使用@Autowired是不生效的。
filterChainDefinitionMap.put("/**", "myAccessControlFilter");
- 1
这行代码是将我上面定义的拦截器具体使用,我用自己定义的拦截器myAccessControlFilter替代了authc(这个是指FormAuthenticationFilter,我有对这个filter重写,但是最后没用到,可以看下filter中的MyFormAuthenticationFilter),使用继承了AccessControlFilter的类来替代FormAuthenticationFilter。
网上的参考内容一般都是说无状态的一般都是通过集成AccessControlFilter来实现的,这里我跟着大部队走。主要还是这个filter的方法,下面会具体介绍。
//自定义加载权限资源关系
// List<Resources> resourcesList = resourcesService.queryAll();
// for(Resources resources:resourcesList){
//
// if (StringUtil.isNotEmpty(resources.getResurl())) {
// String permission = "perms[" + resources.getResurl()+ "]";
// filterChainDefinitionMap.put(resources.getResurl(),permission);
// }
// }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
这块被我备注起来,我参考的代码是用来加载数据库中url与拦截器对应关系的。如果项目中需要单独设置的url多了,最好是在数据库中配置,当然在这里一一列出也好,看个人需求,这里保留的目的只是想说提供了这个功能。
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
return new DefaultAdvisorAutoProxyCreator();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
这两个bean是为了下面这个功能定义的
@RequiresRoles(value={"admin","user"},logical = Logical.OR)
@RequiresPermissions(value={"add","update"},logical = Logical.OR)
- 1
- 2
如果不加这种注解的权限验证不会生效,只能在进入方法后判断了。
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//设置realm.
// securityManager.setAuthenticator(modularRealmAuthenticator());
securityManager.setAuthenticator(customizedModularRealmAuthenticator());
List<Realm> realms=new ArrayList<>();
realms.add(myShiroRealm());
realms.add(myShiroRealm2());
securityManager.setRealms(realms);
return securityManager;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
这个bean是SecurityManager 的设置,用来加载realm,很重要的一个东西(其实这里的bean每个都很重要o( ̄︶ ̄)o)。
这里我给出的代码是实现了多realm的,如果只有一个realm,可以去掉setAuthenticator,这里的realm设置也重新改成单realm(在这里只add一个realm应该也是没问题的,我没试过)。
单realm实现方式如下:
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//设置realm.
securityManager.setRealm(myShiroRealm());
return securityManager;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
很简单的一行代码。
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
//我自己实现的加密判断,这里被备注起来,仅供参考
// myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
realm在这里的加载,由于不能使用@Autowired,这里都是通过@Bean来加载的。备注的这行大家可以留一下,同时也说明了每个realm可以有自己的处理方式。
/**
* 自定义的Realm管理,主要针对多realm
* */
@Bean
public MyModularRealmAuthenticator customizedModularRealmAuthenticator(){
MyModularRealmAuthenticator customizedModularRealmAuthenticator=new MyModularRealmAuthenticator();
//设置realm判断条件
customizedModularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return customizedModularRealmAuthenticator;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
这里是针对多realm设置的,唯一要说的是setAuthenticationStrategy这里,用来设置realm的规则。我这里选用的是AtLeastOneSuccessfulStrategy,至少有一个生效。大家也可以选用AllSuccessfulStrategy()(所有realm都验证通过才能成功登陆)和FirstSuccessfulStrategy()(只要有一个生效就不会去其它realm验证)。
2.MyModularRealmAuthenticator
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
// 判断getRealms()是否返回为空
assertRealmsConfigured();
MyUsernamePasswordToken token=(MyUsernamePasswordToken)authenticationToken;
// 登录类型
String loginType = token.getType();
// 所有Realm
Collection<Realm> realms = getRealms();
// 登录类型对应的所有Realm
Collection<Realm> typeRealms = new ArrayList<>();
HashMap<String,Realm> realmHashMap=new HashMap<>(realms.size());
for (Realm realm : realms) {
realmHashMap.put(realm.getName(),realm);
// if (realm.getName().contains(loginType)){
// typeRealms.add(realm);
// }
}
if (realmHashMap.get(loginType)!=null){
return doSingleRealmAuthentication(realmHashMap.get(loginType), token);
}else {
return doMultiRealmAuthentication(realms, token);
}
// 判断是单Realm还是多Realm
// if (typeRealms.size() == 1)
// return doSingleRealmAuthentication(typeRealms.iterator().next(), token);
// else
// return doMultiRealmAuthentication(typeRealms, token);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
使用多realm时自定义的管理器。参考的代码中给出的示例实现被我备注起来了,他是通过loginType来确定realm的验证类型。我这里该的是可以通过loginType来指定由某个特定的realm来执行这次的验证。
需要说明的是,验证(登陆)通过后在controller的方法中进行验证的时候它是按照setRealms中设定的顺序来找realm进行验证的,直到有一个通过为止。
其实验证的时候如果每个realm对应不同的token,可以通过supports来判断是否经过该realm来验证,这个我代码中的都被我删掉了,因为我只有一种token,当时也没有特别明白这个方法的用途,现在给出来。
@Override
public boolean supports(AuthenticationToken token) {
// return super.supports(token);
仅支持UsernamePasswordToken 类型的Token
System.out.println("token supports");
return token instanceof MyUsernamePasswordToken;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这个方法放到realm中就好,进入realm的时候会第一个执行它。
@Override
public String getName() {
return "myShiroRealm1";
}
- 1
- 2
- 3
- 4
设定realm的名称,多realm中判断登陆类型来指定特定realm的时候我用到的。
3.realm
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
MyUsernamePasswordToken myToken=(MyUsernamePasswordToken)token;
//获取用户的输入的账号.
String username = (String)myToken.getPrincipal();
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
User user = userService.findByUsername(username);
System.out.println(user.toString());
if(user==null){
throw new UnknownAccountException();
}
// if (0==user.getEnable()) {
// // 帐号锁定
// throw new LockedAccountException();
// }
//此处使用的是user对象,不是username
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user,
user.getPassword(),
getName()
);
return authenticationInfo;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
这里是认证功能。
String username = (String)myToken.getPrincipal();
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
User user = userService.findByUsername(username);
- 1
- 2
- 3
第一行是从token中获取登陆的用户名,然后从数据库中取相应的信息。
//此处使用的是user对象,不是username
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user,
user.getPassword(),
getName()
);
- 1
- 2
- 3
- 4
- 5
- 6
这里是对登陆密码做对比的,准确来说是将数据库中的密码user.getPassword()传入,SimpleAuthenticationInfo中有个位置会获取token中的密码与它做对比,如果一样就正常返回,不一样就抛出异常。
user这个字段很有意思,网上很多资料都是传的对象,但是后面备注的都是用户名。但是这里如果传username的时候会报string无法转user对象的异常。但是我在开涛大神那里参考他处理的时候,传username是OK的,传user也是OK的。如果传user,authenticationInfo 就是user的值;如果传username,authenticationInfo 就是一个字符串username的值。所以这里还有一些需要好好研究的地方,如果哪位大神看到了可以给我留言解惑。
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("doGetAuthorizationInfo1");
//User{id=1, username='admin', password='3ef7164d1f6167cb9f2658c07d3c2f0a', enable=1}
User user= (User) SecurityUtils.getSubject().getPrincipal();
List<Permission> permissions=permissionService.findPermissionAndRoleNameByUserId(user.getUserId());
System.out.println("permissions:"+permissions.size());
// List<Role> roles=permissionService.findPermissionByUserId(user.getUserId());
// 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//这里赋给两个不存在的值,使controller中的权限验证失败,验证在此失败会继续进入myShiroRealm2验证权限
// info.addRole("as");
// info.addStringPermission("sdf");
for(Permission permission: permissions){
System.out.println("permission:"+permission.getPermission());
System.out.println("permission.getRoleName():"+permission.getRoleName());
info.addStringPermission(permission.getPermission());
info.addRole(permission.getRoleName());
}
return info;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
这里备注的是授权,可是我感觉这里叫做权限验证更合适(或者调用这里之前有个叫做权限验证的方法,这里只是其中一个被用到的功能),因为这里是在做权限验证的时候用到的,就是那判断用户角色和权限的注解。
主要功能是通过用户信息从数据库中获取相应权限,设置进info中返回(但从返回info这点来看确实是授权的功能)。
4.controller权限验证
@GetMapping("/users")
@RequiresRoles(value={"admin","user"},logical = Logical.OR)
@RequiresPermissions(value={"add","update"},logical = Logical.OR)
public String getUserInfo(){
log.info("into getUserInfo");
// Subject subject= SecurityUtils.getSubject();
// try {
// subject.checkPermissions("add","update");
// }catch (UnauthorizedException e){
// log.info("错误信息:"+e.getMessage());
// //TODO 定义错误处理页面
// log.info("权限不足");
// }
log.info("...............................");
return "Success to get user info";
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
@RequiresRoles(value={"admin","user"},logical = Logical.OR)
@RequiresPermissions(value={"add","update"},logical = Logical.OR)
- 1
- 2
这两行在上面有说过,在进入方法之前判断权限的,很简洁明了。@RequiresRoles和@RequiresPermissions分别对角色和权限进行验证,会调用realm中的doGetAuthorizationInfo。
有几点需要注意,如果需要验证多个角色/权限的时候可以用逗号隔开,如上。默认是所有设置的角色/权限都具有才能验证通过,而且是每个角色/权限验证的时候都会单独调用一次doGetAuthorizationInfo,这是非常耗时的,对性能要求高的需要好好设定。
logical = Logical.OR这个是设置前面设定的角色/权限只要满足一个就可以了。
这篇文章就写到这里,后面可能会写几篇短的文章,来说明我集成过程中遇到的坑。由于我也是最近这几天刚接触,有什么理解不对的地方还请各位大神留言指正。如果有不同观点,也可以在这里探讨。