1.前言
本文主要介绍使用SpringBoot与shiro实现基于数据库的细粒度动态权限管理系统实例。
使用技术:SpringBoot、mybatis、shiro、thymeleaf、pagehelper、Mapper插件、druid、dataTables、ztree、jQuery
开发工具:intellij idea
数据库:mysql、redis
基本上是基于使用SpringSecurity的demo上修改而成,地址 http://blog.csdn.net/poorcoder_/article/details/70231779
2.表结构
还是是用标准的5张表来展现权限。如下图:
分别为用户表,角色表,资源表,用户角色表,角色资源表。在这个demo中使用了mybatis-generator自动生成代码。运行mybatis-generator:generate
-e 根据数据库中的表,生成
相应的model,mapper单表的增删改查。不过如果是导入本项目的就别运行这个命令了。新增表的话,也要修改mybatis-generator-config.xml中的tableName,指定表名再运行。
3.maven配置
1 <?xml version="1.0" encoding="UTF-8"?> 2 3 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 5 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 6 7 <modelVersion>4.0.0</modelVersion> 8 9 10 11 <groupId>com.study</groupId> 12 13 <artifactId>springboot-shiro</artifactId> 14 15 <version>0.0.1-SNAPSHOT</version> 16 17 <packaging>jar</packaging> 18 19 20 21 <name>springboot-shiro</name> 22 23 <description>Demo project for Spring Boot</description> 24 25 26 27 <parent> 28 <groupId>org.springframework.boot</groupId> 29 30 <artifactId>spring-boot-starter-parent</artifactId> 31 32 <version>1.5.2.RELEASE</version> 33 34 <relativePath/> <!-- lookup parent from repository --> 35 36 </parent> 37 38 39 40 <properties> 41 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 42 43 <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> 44 45 <java.version>1.8</java.version> 46 47 </properties> 48 49 50 51 <dependencies> 52 53 <dependency> 54 55 <groupId>org.springframework.boot</groupId> 56 57 <artifactId>spring-boot-starter</artifactId> 58 59 </dependency> 60 61 62 63 <dependency> 64 65 <groupId>org.springframework.boot</groupId> 66 67 <artifactId>spring-boot-starter-test</artifactId> 68 69 <scope>test</scope> 70 71 </dependency> 72 73 <dependency> 74 75 <groupId>org.springframework.boot</groupId> 76 77 <artifactId>spring-boot-starter-web</artifactId> 78 79 </dependency> 80 81 <dependency> 82 83 <groupId>org.springframework.boot</groupId> 84 85 <artifactId>spring-boot-starter-thymeleaf</artifactId> 86 87 </dependency> 88 89 <dependency> 90 91 <groupId>com.github.pagehelper</groupId> 92 93 <artifactId>pagehelper-spring-boot-starter</artifactId> 94 95 <version>1.1.0</version> 96 </dependency> 97 98 <dependency> 99 100 <groupId>tk.mybatis</groupId> 101 102 <artifactId>mapper-spring-boot-starter</artifactId> 103 104 <version>1.1.1</version> 105 106 </dependency> 107 108 <dependency> 109 110 <groupId>org.apache.shiro</groupId> 111 112 <artifactId>shiro-spring</artifactId> 113 114 <version>1.3.2</version> 115 116 </dependency> 117 118 <dependency> 119 120 <groupId>com.alibaba</groupId> 121 122 <artifactId>druid</artifactId> 123 124 <version>1.0.29</version> 125 126 </dependency> 127 128 <dependency> 129 130 <groupId>mysql</groupId> 131 132 <artifactId>mysql-connector-java</artifactId> 133 134 </dependency> 135 136 <dependency> 137 138 <groupId>net.sourceforge.nekohtml</groupId> 139 140 <artifactId>nekohtml</artifactId> 141 142 <version>1.9.22</version> 143 144 </dependency> 145 146 <dependency> 147 148 <groupId>com.github.theborakompanioni</groupId> 149 150 <artifactId>thymeleaf-extras-shiro</artifactId> 151 152 <version>1.2.1</version> 153 154 </dependency> 155 156 <dependency> 157 158 <groupId>org.crazycake</groupId> 159 160 <artifactId>shiro-redis</artifactId> 161 162 <version>2.4.2.1-RELEASE</version> 163 164 </dependency> 165 166 </dependencies> 167 168 169 <build> 170 171 <plugins> 172 173 <plugin> 174 175 <groupId>org.springframework.boot</groupId> 176 177 <artifactId>spring-boot-maven-plugin</artifactId> 178 179 </plugin> 180 181 <plugin> 182 183 <groupId>org.mybatis.generator</groupId> 184 185 <artifactId>mybatis-generator-maven-plugin</artifactId> 186 187 <version>1.3.5</version> 188 189 <configuration> 190 191 <configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile> 192 193 <overwrite>true</overwrite> 194 195 <verbose>true</verbose> 196 197 </configuration> 198 199 <dependencies> 200 201 <dependency> 202 203 <groupId>mysql</groupId> 204 205 <artifactId>mysql-connector-java</artifactId> 206 207 <version>${mysql.version}</version> 208 209 </dependency> 210 211 <dependency> 212 213 <groupId>tk.mybatis</groupId> 214 215 <artifactId>mapper</artifactId> 216 217 <version>3.4.0</version> 218 219 </dependency> 220 221 </dependencies> 222 223 </plugin> 224 225 </plugins> 226 227 </build> 228 229 230 231 232 233 </project>
4.配置Druid
1 package com.study.config; 2 3 4 5 import com.alibaba.druid.support.http.StatViewServlet; 6 7 import com.alibaba.druid.support.http.WebStatFilter; 8 9 import org.springframework.boot.web.servlet.FilterRegistrationBean; 10 11 import org.springframework.boot.web.servlet.ServletRegistrationBean; 12 13 import org.springframework.context.annotation.Bean; 14 15 import org.springframework.context.annotation.Configuration; 16 17 18 19 20 /** 21 22 * Created by yangqj on 2017/4/19. 23 24 */ 25 26 @Configuration 27 28 public class DruidConfig { 29 30 31 @Bean 32 33 public ServletRegistrationBean druidServlet() { 34 35 36 37 ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); 38 39 //登录查看信息的账号密码. 40 41 42 43 servletRegistrationBean.addInitParameter("loginUsername","admin"); 44 45 46 servletRegistrationBean.addInitParameter("loginPassword","123456"); 47 48 return servletRegistrationBean; 49 50 } 51 52 53 @Bean 54 55 public FilterRegistrationBean filterRegistrationBean() { 56 57 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); 58 59 filterRegistrationBean.setFilter(new WebStatFilter()); 60 61 filterRegistrationBean.addUrlPatterns("/*"); 62 63 filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); 64 65 return filterRegistrationBean; 66 67 } 68 69 }
在application.properties中加入:
1 # 数据源基础配置 2 3 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource 4 5 spring.datasource.driver-class-name=com.mysql.jdbc.Driver 6 7 spring.datasource.url=jdbc:mysql://localhost:3306/shiro 8 9 spring.datasource.username=root 10 11 spring.datasource.password=root 12 13 # 连接池配置 14 15 # 初始化大小,最小,最大 16 17 spring.datasource.initialSize=1 18 19 spring.datasource.minIdle=1 20 21 spring.datasource.maxActive=20
配置好后,运行项目访问http://localhost:8080/druid/ 输入配置的账号密码admin,123456进入
5.配置mybatis
使用springboot 整合mybatis非常方便,只需在application.properties
1 mybatis.type-aliases-package=com.study.model 2 3 mybatis.mapper-locations=classpath:mapper/*.xml 4 5 mapper.mappers=com.study.util.MyMapper 6 7 mapper.not-empty=false 8 9 mapper.identity=MYSQL 10 11 pagehelper.helperDialect=mysql 12 13 pagehelper.reasonable=true 14 15 pagehelper.supportMethodsArguments=true 16 17 18 pagehelper.params=count=countSql
将相应的路径改成项目包所在的路径即可。配置文件中可以看出来还加入了pagehelper 和Mapper插件。如果不需要,把上面配置文件中的 pagehelper删除。
MyMapper:
1 package com.study.util; 2 3 4 5 /** 6 7 * Created by yangqj on 2017/4/20. 8 9 */ 10 11 import tk.mybatis.mapper.common.Mapper; 12 13 import tk.mybatis.mapper.common.MySqlMapper; 14 15 public interface MyMapper<T> extends Mapper<T>, MySqlMapper<T> { 16 17 }
对于Springboot整合mybatis可以参考https://github.com/abel533/MyBatis-Spring-Boot
6.thymeleaf配置
thymeleaf是springboot官方推荐的,所以来试一下。
首先加入配置:
1 #spring.thymeleaf.prefix=classpath:/templates/ 2 3 #spring.thymeleaf.suffix=.html 4 5 #spring.thymeleaf.mode=HTML5 6 7 #spring.thymeleaf.encoding=UTF-8 8 9 # ;charset=<encoding> is added 10 11 #spring.thymeleaf.content-type=text/html 12 13 # set to false for hot refresh 14 15 spring.thymeleaf.cache=false 16 17 spring.thymeleaf.mode=LEGACYHTML5
可以看到其实上面都是注释了的,因为springboot会根据约定俗成的方式帮我们配置好。所以上面注释部分是springboot自动配置的,如果需要自定义配置,只需要修改上注释部分即可。
后两行没有注释的部分,spring.thymeleaf.cache=false表示关闭缓存,这样修改文件后不需要重新启动,缓存默认是开启的,所以指定为false。但是在intellij idea中还需要按Ctrl + Shift + F9.
对于spring.thymeleaf.mode=LEGACYHTML5。thymeleaf对html中的语法要求非常严格,像我从网上找的模板,使用thymeleaf后报一堆的语法错误,后来没办法,使用弱语法校验,所以加入配置spring.thymeleaf.mode=LEGACYHTML5。加入这个配置后还需要在maven中加入
1 <dependency> 2 3 <groupId>net.sourceforge.nekohtml</groupId> 4 5 <artifactId>nekohtml</artifactId> 6 7 <version>1.9.22</version> 8 9 </dependency>
否则会报错的。
在前端页面的头部加入一下配置后,就可以使用thymeleaf了
1 <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}" />
不过这个项目因为使用了datatables都是使用jquery 的ajax来访问数据与处理数据,所以用到的thymeleaf语法非常少,基本上可以参考的就是js即css的导入和类似于jsp的include功能的部分页面引入。
对于静态文件的引入:
1 <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}" />
而文件在项目中的位置是static-css-bootstrap.min.css。为什么这样可以访问到该文件,也是因为springboot对于静态文件会自动查找/static public、/resources、/META-INF/resources下的文件。所以不需要加static.
页面引入:
局部页面如下:
1 <div th:fragment="top"> 2 ... 3 </div>
主体页面映入方式:
1 <div th:include="common/top :: top"></div>
inclide=”文件路径::局部代码片段名称”
7.shiro配置
配置文件ShiroConfig
1 package com.study.config; 2 3 4 import at.pollux.thymeleaf.shiro.dialect.ShiroDialect; 5 6 import com.github.pagehelper.util.StringUtil; 7 8 import com.study.model.Resources; 9 10 import com.study.service.ResourcesService; 11 12 import com.study.shiro.MyShiroRealm; 13 14 import org.apache.shiro.authc.credential.HashedCredentialsMatcher; 15 16 import org.apache.shiro.mgt.SecurityManager; 17 18 import org.apache.shiro.spring.LifecycleBeanPostProcessor; 19 20 import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; 21 22 import org.apache.shiro.spring.web.ShiroFilterFactoryBean; 23 24 import org.apache.shiro.web.mgt.DefaultWebSecurityManager; 25 26 import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; 27 28 import org.crazycake.shiro.RedisCacheManager; 29 30 import org.crazycake.shiro.RedisManager; 31 32 import org.crazycake.shiro.RedisSessionDAO; 33 34 import org.springframework.beans.factory.annotation.Autowired; 35 36 import org.springframework.beans.factory.annotation.Value; 37 38 import org.springframework.context.annotation.Bean; 39 40 import org.springframework.context.annotation.Configuration; 41 42 43 44 import java.util.LinkedHashMap; 45 46 import java.util.List; 47 48 import java.util.Map; 49 50 51 52 /** 53 54 * Created by yangqj on 2017/4/23. 55 56 */ 57 58 @Configuration 59 60 public class ShiroConfig { 61 62 @Autowired(required = false) 63 64 private ResourcesService resourcesService; 65 66 67 68 @Value("${spring.redis.host}") 69 70 private String host; 71 72 73 74 @Value("${spring.redis.port}") 75 76 private int port; 77 78 79 80 @Value("${spring.redis.timeout}") 81 82 private int timeout; 83 84 85 86 @Bean 87 88 public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { 89 90 return new LifecycleBeanPostProcessor(); 91 92 } 93 94 95 /** 96 97 * ShiroDialect,为了在thymeleaf里使用shiro的标签的bean 98 99 * @return 100 101 */ 102 103 @Bean 104 105 public ShiroDialect shiroDialect() { 106 107 return new ShiroDialect(); 108 109 } 110 111 /** 112 113 * ShiroFilterFactoryBean 处理拦截资源文件问题。 114 115 * 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在 116 117 * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager 118 119 * 120 121 Filter Chain定义说明 122 123 1、一个URL可以配置多个Filter,使用逗号分隔 124 125 2、当设置多个过滤器时,全部验证通过,才视为通过 126 127 3、部分过滤器可指定参数,如perms,roles 128 129 * 130 131 */ 132 133 @Bean 134 135 public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){ 136 137 System.out.println("ShiroConfiguration.shirFilter()"); 138 139 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); 140 141 142 143 // 必须设置 SecurityManager 144 145 shiroFilterFactoryBean.setSecurityManager(securityManager); 146 147 // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 148 149 shiroFilterFactoryBean.setLoginUrl("/login"); 150 151 // 登录成功后要跳转的链接 152 153 shiroFilterFactoryBean.setSuccessUrl("/usersPage"); 154 155 //未授权界面; 156 157 shiroFilterFactoryBean.setUnauthorizedUrl("/403"); 158 159 //拦截器. 160 161 Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>(); 162 163 164 165 //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了 166 167 filterChainDefinitionMap.put("/logout", "logout"); 168 169 filterChainDefinitionMap.put("/css/**","anon"); 170 171 filterChainDefinitionMap.put("/js/**","anon"); 172 173 filterChainDefinitionMap.put("/img/**","anon"); 174 175 filterChainDefinitionMap.put("/font-awesome/**","anon"); 176 177 //<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了; 178 179 //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问--> 180 181 //自定义加载权限资源关系 182 183 List<Resources> resourcesList = resourcesService.queryAll(); 184 185 for(Resources resources:resourcesList){ 186 187 188 189 if (StringUtil.isNotEmpty(resources.getResurl())) { 190 191 String permission = "perms[" + resources.getResurl()+ "]"; 192 193 filterChainDefinitionMap.put(resources.getResurl(),permission); 194 195 } 196 197 } 198 199 filterChainDefinitionMap.put("/**", "authc"); 200 201 202 203 204 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); 205 206 return shiroFilterFactoryBean; 207 208 } 209 210 211 212 @Bean 213 214 public SecurityManager securityManager(){ 215 216 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); 217 218 //设置realm. 219 220 securityManager.setRealm(myShiroRealm()); 221 222 // 自定义缓存实现 使用redis 223 224 //securityManager.setCacheManager(cacheManager()); 225 226 // 自定义session管理 使用redis 227 228 securityManager.setSessionManager(sessionManager()); 229 230 return securityManager; 231 232 } 233 234 235 236 @Bean 237 238 public MyShiroRealm myShiroRealm(){ 239 240 MyShiroRealm myShiroRealm = new MyShiroRealm(); 241 242 myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); 243 244 return myShiroRealm; 245 246 } 247 248 249 250 /** 251 252 * 凭证匹配器 253 * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 254 255 * 所以我们需要修改下doGetAuthenticationInfo中的代码; 256 257 * ) 258 259 * @return 260 261 */ 262 263 @Bean 264 265 public HashedCredentialsMatcher hashedCredentialsMatcher(){ 266 267 HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); 268 269 270 271 hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法; 272 273 hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); 274 275 276 277 return hashedCredentialsMatcher; 278 279 } 280 281 282 283 /** 284 285 * 开启shiro aop注解支持. 286 287 * 使用代理方式;所以需要开启代码支持; 288 289 * @param securityManager 290 * @return 291 292 */ 293 294 @Bean 295 296 public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ 297 298 AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); 299 300 authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); 301 302 return authorizationAttributeSourceAdvisor; 303 304 } 305 306 307 /** 308 309 * 配置shiro redisManager 310 311 * 使用的是shiro-redis开源插件 312 313 * @return 314 315 */ 316 317 public RedisManager redisManager() { 318 319 RedisManager redisManager = new RedisManager(); 320 321 redisManager.setHost(host); 322 323 redisManager.setPort(port); 324 325 redisManager.setExpire(1800);// 配置缓存过期时间 326 327 redisManager.setTimeout(timeout); 328 329 // redisManager.setPassword(password); 330 331 return redisManager; 332 333 } 334 335 336 337 /** 338 339 * cacheManager 缓存 redis实现 340 341 * 使用的是shiro-redis开源插件 342 343 * @return 344 345 */ 346 347 public RedisCacheManager cacheManager() { 348 349 RedisCacheManager redisCacheManager = new RedisCacheManager(); 350 351 redisCacheManager.setRedisManager(redisManager()); 352 353 return redisCacheManager; 354 355 } 356 357 358 359 /** 360 361 * RedisSessionDAO shiro sessionDao层的实现 通过redis 362 363 * 使用的是shiro-redis开源插件 364 365 */ 366 367 @Bean 368 369 public RedisSessionDAO redisSessionDAO() { 370 371 RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); 372 373 redisSessionDAO.setRedisManager(redisManager()); 374 375 return redisSessionDAO; 376 377 } 378 379 380 /** 381 382 * shiro session的管理 383 384 */ 385 386 @Bean 387 388 public DefaultWebSessionManager sessionManager() { 389 390 DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); 391 392 sessionManager.setSessionDAO(redisSessionDAO()); 393 394 return sessionManager; 395 396 } 397 398 399 }
配置自定义Realm
1 package com.study.shiro; 2 3 4 import com.study.model.Resources; 5 6 import com.study.model.User; 7 8 import com.study.service.ResourcesService; 9 10 import com.study.service.UserService; 11 12 import org.apache.shiro.SecurityUtils; 13 14 import org.apache.shiro.authc.*; 15 16 import org.apache.shiro.authz.AuthorizationInfo; 17 18 import org.apache.shiro.authz.SimpleAuthorizationInfo; 19 20 import org.apache.shiro.realm.AuthorizingRealm; 21 22 import org.apache.shiro.session.Session; 23 24 import org.apache.shiro.subject.PrincipalCollection; 25 26 import org.apache.shiro.util.ByteSource; 27 28 29 30 import javax.annotation.Resource; 31 32 import java.util.HashMap; 33 34 import java.util.List; 35 36 import java.util.Map; 37 38 39 /** 40 41 * Created by yangqj on 2017/4/21. 42 43 */ 44 45 public class MyShiroRealm extends AuthorizingRealm { 46 47 48 49 @Resource 50 51 private UserService userService; 52 53 54 55 @Resource 56 57 private ResourcesService resourcesService; 58 59 60 //授权 61 62 @Override 63 64 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { 65 66 User user= (User) SecurityUtils.getSubject().getPrincipal();//User{id=1, username='admin', password='3ef7164d1f6167cb9f2658c07d3c2f0a', enable=1} 67 68 Map<String,Object> map = new HashMap<String,Object>(); 69 70 map.put("userid",user.getId()); 71 72 List<Resources> resourcesList = resourcesService.loadUserResources(map); 73 74 // 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission) 75 76 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); 77 78 for(Resources resources: resourcesList){ 79 80 info.addStringPermission(resources.getResurl()); 81 82 } 83 84 return info; 85 86 } 87 88 89 //认证 90 91 @Override 92 93 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { 94 95 //获取用户的输入的账号. 96 97 String username = (String)token.getPrincipal(); 98 99 User user = userService.selectByUsername(username); 100 101 if(user==null) throw new UnknownAccountException(); 102 103 if (0==user.getEnable()) { 104 105 throw new LockedAccountException(); // 帐号锁定 106 107 } 108 109 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( 110 111 user, //用户 112 113 user.getPassword(), //密码 114 115 ByteSource.Util.bytes(username), 116 117 getName() //realm name 118 119 ); 120 121 // 当验证都通过后,把用户信息放在session里 122 123 Session session = SecurityUtils.getSubject().getSession(); 124 125 session.setAttribute("userSession", user); 126 127 session.setAttribute("userSessionId", user.getId()); 128 129 return authenticationInfo; 130 131 } 132 133 134 135 136 }
认证:
shiro的主要模块分别就是授权和认证和会话管理。
我们先讲认证。认证就是验证用户。比如用户登录的时候验证账号密码是否正确。
我们可以把对登录的验证交给shiro。我们执行要查询相应的用户信息,并传给shiro。如下代码则为用户登录:
1 @RequestMapping(value="/login",method=RequestMethod.POST) 2 3 public String login(HttpServletRequest request, User user, Model model){ 4 5 if (StringUtils.isEmpty(user.getUsername()) || StringUtils.isEmpty(user.getPassword())) { 6 7 request.setAttribute("msg", "用户名或密码不能为空!"); 8 9 return "login"; 10 11 } 12 13 Subject subject = SecurityUtils.getSubject(); 14 15 UsernamePasswordToken token=new UsernamePasswordToken(user.getUsername(),user.getPassword()); 16 17 try { 18 19 subject.login(token); 20 21 return "redirect:usersPage"; 22 23 }catch (LockedAccountException lae) { 24 25 token.clear(); 26 27 request.setAttribute("msg", "用户已经被锁定不能登录,请与管理员联系!"); 28 29 return "login"; 30 31 } catch (AuthenticationException e) { 32 33 token.clear(); 34 35 request.setAttribute("msg", "用户或密码不正确!"); 36 37 return "login"; 38 39 }
可见用户登陆的代码主要就是 subject.login(token);调用后就会进去我们自定义的realm中的doGetAuthenticationInfo()方法。
1 //认证 2 3 @Override 4 5 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { 6 7 //获取用户的输入的账号. 8 9 String username = (String)token.getPrincipal(); 10 11 User user = userService.selectByUsername(username); 12 13 if(user==null) throw new UnknownAccountException(); 14 15 if (0==user.getEnable()) { 16 17 throw new LockedAccountException(); // 帐号锁定 18 19 } 20 21 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( 22 23 user, //用户 24 25 user.getPassword(), //密码 26 27 ByteSource.Util.bytes(username), 28 29 getName() //realm name 30 31 ); 32 33 // 当验证都通过后,把用户信息放在session里 34 35 Session session = SecurityUtils.getSubject().getSession(); 36 37 session.setAttribute("userSession", user); 38 39 session.setAttribute("userSessionId", user.getId()); 40 return authenticationInfo; 41 42 }
而我们在ShiroConfig中配置了凭证匹配器:
1 @Bean 2 3 public MyShiroRealm myShiroRealm(){ 4 5 MyShiroRealm myShiroRealm = new MyShiroRealm(); 6 7 myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); 8 9 return myShiroRealm; 10 11 } 12 13 14 15 @Bean 16 public HashedCredentialsMatcher hashedCredentialsMatcher(){ 17 18 HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); 19 20 21 22 hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法; 23 24 hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); 25 26 27 return hashedCredentialsMatcher; 28 29 }
所以在认证时的密码是加过密的,使用md5散发将密码与盐值组合加密两次。则我们在增加用户的时候,对用户的密码则要进过相同规则的加密才行。
添加用户代码如下:
1 @RequestMapping(value = "/add") 2 3 public String add(User user) { 4 5 User u = userService.selectByUsername(user.getUsername()); 6 7 if(u != null) 8 9 return "error"; 10 11 try { 12 13 user.setEnable(1); 14 15 PasswordHelper passwordHelper = new PasswordHelper(); 16 17 passwordHelper.encryptPassword(user); 18 19 userService.save(user); 20 21 return "success"; 22 23 } catch (Exception e) { 24 25 e.printStackTrace(); 26 27 return "fail"; 28 29 } 30 31 }
PasswordHelper:
1 package com.study.util; 2 3 4 5 import com.study.model.User; 6 7 import org.apache.shiro.crypto.RandomNumberGenerator; 8 9 import org.apache.shiro.crypto.SecureRandomNumberGenerator; 10 11 import org.apache.shiro.crypto.hash.SimpleHash; 12 import org.apache.shiro.util.ByteSource; 13 14 15 public class PasswordHelper { 16 17 //private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator(); 18 19 private String algorithmName = "md5"; 20 21 private int hashIterations = 2; 22 23 24 25 public void encryptPassword(User user) { 26 27 //String salt=randomNumberGenerator.nextBytes().toHex(); 28 29 String newPassword = new SimpleHash(algorithmName, user.getPassword(), ByteSource.Util.bytes(user.getUsername()), hashIterations).toHex(); 30 31 //String newPassword = new SimpleHash(algorithmName, user.getPassword()).toHex(); 32 33 user.setPassword(newPassword); 34 35 36 37 } 38 39 public static void main(String[] args) { 40 41 PasswordHelper passwordHelper = new PasswordHelper(); 42 43 User user = new User(); 44 45 user.setUsername("admin"); 46 47 user.setPassword("admin"); 48 49 passwordHelper.encryptPassword(user); 50 51 System.out.println(user); 52 53 } 54 55 }
授权:
接下来讲下授权。在自定义relalm中的代码为:
1 //授权 2 3 @Override 4 5 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { 6 7 User user= (User) SecurityUtils.getSubject().getPrincipal();//User{id=1, username='admin', password='3ef7164d1f6167cb9f2658c07d3c2f0a', enable=1} 8 9 Map<String,Object> map = new HashMap<String,Object>(); 10 11 map.put("userid",user.getId()); 12 13 List<Resources> resourcesList = resourcesService.loadUserResources(map); 14 // 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission) 15 16 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); 17 18 for(Resources resources: resourcesList){ 19 20 info.addStringPermission(resources.getResurl()); 21 22 } 23 24 return info; 25 26 }
从以上代码中可以看出来,我根据用户id查询出用户的权限,放入SimpleAuthorizationInfo。关联表user_role,role_resources,resources,三张表,根据用户所拥有的角色,角色所拥有的权限,查询出分配给该用户的所有权限的url。当访问的链接中配置在shiro中时,或者使用shiro标签,shiro权限注解时,则会访问该方法,判断该用户是否拥有相应的权限。
在ShiroConfig中有如下代码:
1 @Bean 2 3 public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){ 4 5 System.out.println("ShiroConfiguration.shirFilter()"); 6 7 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); 8 9 10 11 // 必须设置 SecurityManager 12 13 shiroFilterFactoryBean.setSecurityManager(securityManager); 14 15 // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 16 17 shiroFilterFactoryBean.setLoginUrl("/login"); 18 19 // 登录成功后要跳转的链接 20 21 shiroFilterFactoryBean.setSuccessUrl("/usersPage"); 22 23 //未授权界面; 24 25 shiroFilterFactoryBean.setUnauthorizedUrl("/403"); 26 27 //拦截器. 28 29 Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>(); 30 31 32 //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了 33 34 filterChainDefinitionMap.put("/logout", "logout"); 35 36 filterChainDefinitionMap.put("/css/**","anon"); 37 38 filterChainDefinitionMap.put("/js/**","anon"); 39 40 filterChainDefinitionMap.put("/img/**","anon"); 41 42 filterChainDefinitionMap.put("/font-awesome/**","anon"); 43 44 //<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了; 45 46 //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问--> 47 48 //自定义加载权限资源关系 49 50 List<Resources> resourcesList = resourcesService.queryAll(); 51 for(Resources resources:resourcesList){ 52 53 54 55 if (StringUtil.isNotEmpty(resources.getResurl())) { 56 57 String permission = "perms[" + resources.getResurl()+ "]"; 58 59 filterChainDefinitionMap.put(resources.getResurl(),permission); 60 61 } 62 63 } 64 65 filterChainDefinitionMap.put("/**", "authc"); 66 67 68 69 70 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); 71 72 return shiroFilterFactoryBean; 73 74 }
该代码片段为配置shiro的过滤器。以上代码将静态文件设置为任何权限都可访问,然后
1 List<Resources> resourcesList = resourcesService.queryAll(); 2 3 for(Resources resources:resourcesList){ 4 5 6 7 if (StringUtil.isNotEmpty(resources.getResurl())) { 8 String permission = "perms[" + resources.getResurl()+ "]"; 9 10 filterChainDefinitionMap.put(resources.getResurl(),permission); 11 12 } 13 14 }
在数据中查询所有的资源,将该资源的url当作key,配置拥有该url权限的用户才可访问该url。
最后加入 filterChainDefinitionMap.put(“/*”, “authc”);表示其他没有配置的链接都需要认证才可访问。注意这个要放最后面,因为shiro的匹配是从上往下,如果匹配到就不继续匹配了,所以把 /放到最前面,则 后面的链接都无法匹配到了。
而这段代码是在项目启动的时候加载的。加载的数据是放到内存中的。但是当权限增加或者删除时,正常情况下不会重新启动来,重新加载权限。所以需要调用以下代码的updatePermission()方法来重新加载权限。其实下面的代码有些重复了,可以稍微调整下,我就先这么写了。
1 package com.study.shiro; 2 3 4 import com.github.pagehelper.util.StringUtil; 5 6 import com.study.model.Resources; 7 8 import com.study.model.User; 9 10 import com.study.service.ResourcesService; 11 12 import org.apache.shiro.SecurityUtils; 13 14 import org.apache.shiro.mgt.RealmSecurityManager; 15 16 import org.apache.shiro.session.Session; 17 18 import org.apache.shiro.spring.web.ShiroFilterFactoryBean; 19 20 import org.apache.shiro.subject.SimplePrincipalCollection; 21 22 import org.apache.shiro.subject.support.DefaultSubjectContext; 23 24 import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager; 25 26 import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver; 27 28 import org.apache.shiro.web.servlet.AbstractShiroFilter; 29 import org.crazycake.shiro.RedisSessionDAO; 30 31 import org.springframework.beans.factory.annotation.Autowired; 32 33 import org.springframework.stereotype.Service; 34 35 36 37 import java.util.*; 38 39 40 /** 41 42 * Created by yangqj on 2017/4/30. 43 44 */ 45 46 @Service 47 48 public class ShiroService { 49 50 @Autowired 51 52 private ShiroFilterFactoryBean shiroFilterFactoryBean; 53 54 @Autowired 55 56 private ResourcesService resourcesService; 57 58 @Autowired 59 60 private RedisSessionDAO redisSessionDAO; 61 62 /** 63 64 * 初始化权限 65 66 */ 67 68 public Map<String, String> loadFilterChainDefinitions() { 69 70 // 权限控制map.从数据库获取 71 72 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); 73 74 filterChainDefinitionMap.put("/logout", "logout"); 75 76 filterChainDefinitionMap.put("/css/**","anon"); 77 78 filterChainDefinitionMap.put("/js/**","anon"); 79 80 filterChainDefinitionMap.put("/img/**","anon"); 81 82 filterChainDefinitionMap.put("/font-awesome/**","anon"); 83 84 List<Resources> resourcesList = resourcesService.queryAll(); 85 86 for(Resources resources:resourcesList){ 87 88 89 90 if (StringUtil.isNotEmpty(resources.getResurl())) { 91 92 String permission = "perms[" + resources.getResurl()+ "]"; 93 94 filterChainDefinitionMap.put(resources.getResurl(),permission); 95 96 } 97 98 } 99 100 filterChainDefinitionMap.put("/**", "authc"); 101 102 return filterChainDefinitionMap; 103 104 } 105 106 107 /** 108 * 重新加载权限 109 */ 110 111 public void updatePermission() { 112 113 114 115 synchronized (shiroFilterFactoryBean) { 116 117 118 119 AbstractShiroFilter shiroFilter = null; 120 121 try { 122 123 shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean 124 125 .getObject(); 126 127 } catch (Exception e) { 128 129 throw new RuntimeException( 130 131 "get ShiroFilter from shiroFilterFactoryBean error!"); 132 133 } 134 135 136 137 PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter 138 139 .getFilterChainResolver(); 140 141 DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver 142 143 .getFilterChainManager(); 144 145 146 147 // 清空老的权限控制 148 149 manager.getFilterChains().clear(); 150 151 152 153 shiroFilterFactoryBean.getFilterChainDefinitionMap().clear(); 154 155 shiroFilterFactoryBean 156 157 .setFilterChainDefinitionMap(loadFilterChainDefinitions()); 158 159 // 重新构建生成 160 161 Map<String, String> chains = shiroFilterFactoryBean 162 163 .getFilterChainDefinitionMap(); 164 165 for (Map.Entry<String, String> entry : chains.entrySet()) { 166 167 String url = entry.getKey(); 168 169 String chainDefinition = entry.getValue().trim() 170 171 .replace(" ", ""); 172 173 manager.createChain(url, chainDefinition); 174 175 } 176 177 178 179 System.out.println("更新权限成功!!"); 180 181 } 182 183 } 184 185 186 187 188 }
会话管理
这个例子使用了redis保存session。这样可以实现集群的session共享。在ShiroConfig中有代码:
1 @Bean 2 3 public SecurityManager securityManager(){ 4 5 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); 6 7 //设置realm. 8 9 securityManager.setRealm(myShiroRealm()); 10 11 // 自定义缓存实现 使用redis 12 13 //securityManager.setCacheManager(cacheManager()); 14 15 // 自定义session管理 使用redis 16 17 securityManager.setSessionManager(sessionManager()); 18 19 return securityManager; 20 21 }
配置了自定义session,网上已经有大神实现了 使用redis 自定义session管理,直接拿来用,引入包
1 <dependency> 2 3 <groupId>org.crazycake</groupId> 4 5 <artifactId>shiro-redis</artifactId> 6 7 <version>2.4.2.1-RELEASE</version> 8 9 </dependency>
然后再配置:
1 /** 2 3 * 配置shiro redisManager 4 5 * 使用的是shiro-redis开源插件 6 7 * @return 8 9 */ 10 11 public RedisManager redisManager() { 12 13 RedisManager redisManager = new RedisManager(); 14 15 redisManager.setHost(host); 16 17 redisManager.setPort(port); 18 19 redisManager.setExpire(1800);// 配置缓存过期时间 20 21 redisManager.setTimeout(timeout); 22 23 // redisManager.setPassword(password); 24 25 return redisManager; 26 27 } 28 29 30 /** 31 32 * cacheManager 缓存 redis实现 33 34 * 使用的是shiro-redis开源插件 35 36 * @return 37 38 */ 39 40 public RedisCacheManager cacheManager() { 41 42 RedisCacheManager redisCacheManager = new RedisCacheManager(); 43 44 redisCacheManager.setRedisManager(redisManager()); 45 46 return redisCacheManager; 47 48 } 49 50 51 52 53 /** 54 55 * RedisSessionDAO shiro sessionDao层的实现 通过redis 56 57 * 使用的是shiro-redis开源插件 58 59 */ 60 61 @Bean 62 63 public RedisSessionDAO redisSessionDAO() { 64 65 RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); 66 67 redisSessionDAO.setRedisManager(redisManager()); 68 69 return redisSessionDAO; 70 71 } 72 73 74 /** 75 76 * shiro session的管理 77 78 */ 79 80 @Bean 81 82 public DefaultWebSessionManager sessionManager() { 83 84 DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); 85 86 sessionManager.setSessionDAO(redisSessionDAO()); 87 88 return sessionManager; 89 90 }
RedisConfig
1 package com.study.config; 2 3 4 import org.apache.log4j.Logger; 5 6 import org.springframework.beans.factory.annotation.Value; 7 8 import org.springframework.cache.annotation.CachingConfigurerSupport; 9 10 import org.springframework.cache.annotation.EnableCaching; 11 12 import org.springframework.context.annotation.Bean; 13 14 import org.springframework.context.annotation.Configuration; 15 16 import redis.clients.jedis.JedisPool; 17 18 import redis.clients.jedis.JedisPoolConfig; 19 20 21 /** 22 23 * Created by yangqj on 2017/4/30. 24 */ 25 @Configuration 26 27 @EnableCaching 28 29 public class RedisConfig extends CachingConfigurerSupport { 30 @Value("${spring.redis.host}") 31 32 private String host; 33 34 35 36 @Value("${spring.redis.port}") 37 38 private int port; 39 40 41 42 @Value("${spring.redis.timeout}") 43 44 private int timeout; 45 46 47 @Value("${spring.redis.pool.max-idle}") 48 49 private int maxIdle; 50 51 52 53 @Value("${spring.redis.pool.max-wait}") 54 55 private long maxWaitMillis; 56 57 58 59 @Bean 60 61 public JedisPool redisPoolFactory() { 62 63 Logger.getLogger(getClass()).info("JedisPool注入成功!!"); 64 65 Logger.getLogger(getClass()).info("redis地址:" + host + ":" + port); 66 67 JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); 68 69 jedisPoolConfig.setMaxIdle(maxIdle); 70 71 jedisPoolConfig.setMaxWaitMillis(maxWaitMillis); 72 73 74 JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout); 75 76 77 return jedisPool; 78 79 } 80 81 82 }
配置文件 application.properties中加入:
1 #redis 2 3 # Redis服务器地址 4 5 spring.redis.host= localhost 6 7 # Redis服务器连接端口 8 9 spring.redis.port= 6379 10 11 # 连接池中的最大空闲连接 12 13 spring.redis.pool.max-idle= 8 14 15 # 连接池中的最小空闲连接 16 17 spring.redis.pool.min-idle= 0 18 19 # 连接池最大连接数(使用负值表示没有限制) 20 21 spring.redis.pool.max-active= 8 22 23 # 连接池最大阻塞等待时间(使用负值表示没有限制) 24 25 spring.redis.pool.max-wait= -1 26 27 # 连接超时时间(毫秒) 28 spring.redis.timeout= 0
当然运行的时候要先启动redis。将自己的redis配置在以上配置中。这样session就存在redis中了。
上面ShiroConfig中的securityManager()方法中,我把
1 //securityManager.setCacheManager(cacheManager());
这行代码注了,是这样的,因为每次在需要验证的地方,比如在subject.hasRole(“admin”) 或 subject.isPermitted(“admin”)、@RequiresRoles(“admin”) 、 shiro:hasPermission=”/users/add”的时候都会调用MyShiroRealm中的doGetAuthorizationInfo()。
但是以为这些信息不是经常变的,所以有必要进行缓存。把这行代码的注释打开,的时候都会调用MyShiroRealm中的doGetAuthorizationInfo()的返回结果会被redis缓存。但是这里稍微有个小问题,就是在刚修改用户的权限时,无法立即失效
。本来我是使用了ShiroService中的clearUserAuthByUserId()想清除当前session存在的用户的权限缓存,但是没有效果。
不知道什么原因。希望哪个大神看到后帮忙弄个解决方法。所以我干脆就把doGetAuthorizationInfo()的返回结果通过spring cache的方式加入缓存。
1 @Cacheable(cacheNames="resources",key="#map['userid'].toString()+#map['type']") 2 public List<Resources> loadUserResources(Map<String, Object> map) { 3 4 return resourcesMapper.loadUserResources(map); 5 6 }
这样也可以实现,然后在修改权限时加上注解
1 @CacheEvict(cacheNames="resources", allEntries=true)
这样修改权限后可以立即生效。其实我感觉这样不好,因为清楚了我是清除了所有用户的权限缓存,其实只要修改当前session在线中被修改权限的用户就行了。 先这样吧,以后再研究下,修改得更好一点。
按钮控制
在前端页面,对按钮进行细粒度权限控制,只需要在按钮上加上shiro:hasPermission
1 <button shiro:hasPermission="/users/add" type="button" onclick="$('#addUser').modal();" class="btn btn-info" >新增</button>
这里的参数就是我们在ShiroConfig-shirFilter()权限加载时的过滤器 中的value,也就是资源的url。
1 filterChainDefinitionMap.put(resources.getResurl(),permission);
8.效果图
9.运行、下载
下载项目后运行resources下的shiro.sql文件。需要运行redis后运行项目。访问http://localhost:8080/ 账号密码:admin admin 或user1 user1.新增的用户也可以登录。
github下载地址:https://github.com/lovelyCoder/springboot-shiro
转自:https://www.cnblogs.com/jpfss/p/8311317.html#