SpringBoot
是为了简化 Spring
应用的创建、运行、调试、部署等一系列问题而诞生的产物,自动装配的特性让我们可以更好的关注业务本身而不是外部的XML配置,我们只需遵循规范,引入相关的依赖就可以轻易的搭建出一个 WEB 工程
Shiro 是 Apache
旗下开源的一款强大且易用的Java安全框架,身份验证、授权、加密、会话管理。 相比 Spring Security
而言 Shiro
更加轻量级,且 API 更易于理解…
Shiro Shiro
主要分为 安全认证 和 接口授权 两个部分,其中的核心组件为 Subject
、SecurityManager
、Realms
,公共部分 Shiro
都已经为我们封装好了,我们只需要按照一定的规则去编写响应的代码即可…
Subject
即表示主体,将用户的概念理解为当前操作的主体,因为它即可以是一个通过浏览器请求的用户,也可能是一个运行的程序,外部应用与 Subject 进行交互,记录当前操作用户。Subject 代表了当前用户的安全操作,SecurityManager 则管理所有用户的安全操作。
SecurityManager
即安全管理器,对所有的 Subject 进行安全管理,并通过它来提供安全管理的各种服务(认证、授权等)
Realm
充当了应用与数据安全间的 桥梁 或 连接器 。当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。
本章目标 利用 Spring Boot
与 Shiro
实现安全认证和授权….
导入依赖 依赖 spring-boot-starter-web
…
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 <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding > <java.version > 1.8</java.version > <shiro.version > 1.4.0</shiro.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-core</artifactId > <version > ${shiro.version}</version > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-spring</artifactId > <version > ${shiro.version}</version > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-ehcache</artifactId > <version > ${shiro.version}</version > </dependency > </dependencies >
属性配置 缓存配置 Shiro 为我们提供了 CacheManager
即缓存管理,将用户权限数据存储在缓存,可以提高它的性能。支持 EhCache
、Redis
等常规缓存,这里为了简单起见就用 EhCache
了 , 在resources
目录下创建一个 ehcache-shiro.xml
文件
1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="UTF-8"?> <ehcache updateCheck ="false" name ="shiroCache" > <defaultCache maxElementsInMemory ="10000" eternal ="false" timeToIdleSeconds ="120" timeToLiveSeconds ="120" overflowToDisk ="false" diskPersistent ="false" diskExpiryThreadIntervalSeconds ="120" /> </ehcache >
实体类 创建一个 User.java
,标记为数据库用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.battcn.entity;public class User { private Long id; private String username; private String password; private String roleName; private boolean locked; }
伪造数据 支持 roles
、permissions
,比如你一个接口可以允许用户拥有某一个角色,也可以是拥有某一个 permission
…
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 package com.battcn.config;import com.battcn.entity.User;import java.util.*;public class DBCache { public static final Map<String, User> USERS_CACHE = new HashMap<>(); public static final Map<String, Collection<String>> PERMISSIONS_CACHE = new HashMap<>(); static { USERS_CACHE.put("u1" , new User(1L , "u1" , "p1" , "admin" , true )); USERS_CACHE.put("u2" , new User(2L , "u2" , "p2" , "admin" , false )); USERS_CACHE.put("u3" , new User(3L , "u3" , "p3" , "test" , true )); PERMISSIONS_CACHE.put("admin" , Arrays.asList("user:list" , "user:add" , "user:edit" )); PERMISSIONS_CACHE.put("test" , Collections.singletonList("user:list" )); } }
ShiroConfiguration Shiro 的主要配置信息都在此文件内实现;
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 package com.battcn.config;import org.apache.shiro.cache.ehcache.EhCacheManager;import org.apache.shiro.spring.LifecycleBeanPostProcessor;import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;import org.apache.shiro.spring.web.ShiroFilterFactoryBean;import org.apache.shiro.web.mgt.DefaultWebSecurityManager;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.util.LinkedHashMap;import java.util.Map;@Configuration public class ShiroConfiguration { private static final Logger log = LoggerFactory.getLogger(ShiroConfiguration.class); @Bean public EhCacheManager getEhCacheManager () { EhCacheManager em = new EhCacheManager(); em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml" ); return em; } @Bean (name = "lifecycleBeanPostProcessor" ) public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor () { return new LifecycleBeanPostProcessor(); } @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator () { DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); autoProxyCreator.setProxyTargetClass(true ); return autoProxyCreator; } @Bean (name = "authRealm" ) public AuthRealm authRealm (EhCacheManager cacheManager) { AuthRealm authRealm = new AuthRealm(); authRealm.setCacheManager(cacheManager); return authRealm; } @Bean (name = "securityManager" ) public DefaultWebSecurityManager getDefaultWebSecurityManager (AuthRealm authRealm) { DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); defaultWebSecurityManager.setRealm(authRealm); defaultWebSecurityManager.setCacheManager(getEhCacheManager()); return defaultWebSecurityManager; } @Bean public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor ( DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } @Bean (name = "shiroFilter" ) public ShiroFilterFactoryBean getShiroFilterFactoryBean (DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login" ); shiroFilterFactoryBean.setSuccessUrl("/index" ); shiroFilterFactoryBean.setUnauthorizedUrl("/denied" ); loadShiroFilterChain(shiroFilterFactoryBean); return shiroFilterFactoryBean; } private void loadShiroFilterChain (ShiroFilterFactoryBean shiroFilterFactoryBean) { Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/resource/**" , "anon" ); filterChainDefinitionMap.put("/install" , "anon" ); filterChainDefinitionMap.put("/hello" , "anon" ); log.info("##################从数据库读取权限规则,加载到shiroFilter中##################" ); Map<String, String> permissions = new LinkedHashMap<>(); permissions.put("/users/find" , "perms[user:find]" ); filterChainDefinitionMap.putAll(permissions); filterChainDefinitionMap.put("/**" , "authc" ); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); } }
AuthRealm 上面介绍过 Realm
,安全认证和权限验证的核心处理就是重写 AuthorizingRealm
中的 doGetAuthenticationInfo(登录认证)
与 doGetAuthorizationInfo(权限验证)
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 package com.battcn.config;import com.battcn.entity.User;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.*;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.authz.SimpleAuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.session.Session;import org.apache.shiro.subject.PrincipalCollection;import org.apache.shiro.util.ByteSource;import org.springframework.context.annotation.Configuration;import java.util.*;@Configuration public class AuthRealm extends AuthorizingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); User user = Optional.ofNullable(DBCache.USERS_CACHE.get(principal)).orElseThrow(UnknownAccountException::new ); if (!user.isLocked()) { throw new LockedAccountException(); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, user.getPassword(), getName()); Session session = SecurityUtils.getSubject().getSession(); session.setAttribute("USER_SESSION" , user); return authenticationInfo; } @Override protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principal) { Session session = SecurityUtils.getSubject().getSession(); User user = (User) session.getAttribute("USER_SESSION" ); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Set<String> roles = new HashSet<>(); roles.add(user.getRoleName()); info.setRoles(roles); final Map<String, Collection<String>> permissionsCache = DBCache.PERMISSIONS_CACHE; final Collection<String> permissions = permissionsCache.get(user.getRoleName()); info.addStringPermissions(permissions); return info; } }
控制器 在 ShiroConfiguration
中的 shiroFilter
处配置了 /hello = anon
,意味着可以不需要认证也可以访问,那么除了这种方式外 Shiro
还为我们提供了一些注解相关的方式…
常用注解
@RequiresGuest
代表无需认证即可访问,同理的就是 /path = anon
@RequiresAuthentication
需要认证,只要登录成功后就允许你操作
@RequiresPermissions
需要特定的权限,没有则抛出AuthorizationException
@RequiresRoles
需要特定的橘色,没有则抛出AuthorizationException
@RequiresUser
不太清楚,不常用…
LoginController 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 59 60 61 62 63 64 65 66 67 68 69 package com.battcn.controller;import com.battcn.config.ShiroConfiguration;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.*;import org.apache.shiro.subject.Subject;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.servlet.mvc.support.RedirectAttributes;@RestController public class LoginController { private static final Logger log = LoggerFactory.getLogger(ShiroConfiguration.class); @GetMapping (value = "/hello" ) public String hello () { log.info("不登录也可以访问..." ); return "hello..." ; } @GetMapping (value = "/index" ) public String index () { log.info("登陆成功了..." ); return "index" ; } @GetMapping (value = "/denied" ) public String denied () { log.info("小伙子权限不足,别无谓挣扎了..." ); return "denied..." ; } @GetMapping (value = "/login" ) public String login (String username, String password, RedirectAttributes model) { Subject sub = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { sub.login(token); } catch (UnknownAccountException e) { log.error("对用户[{}]进行登录验证,验证未通过,用户不存在" , username); token.clear(); return "UnknownAccountException" ; } catch (LockedAccountException lae) { log.error("对用户[{}]进行登录验证,验证未通过,账户已锁定" , username); token.clear(); return "LockedAccountException" ; } catch (ExcessiveAttemptsException e) { log.error("对用户[{}]进行登录验证,验证未通过,错误次数过多" , username); token.clear(); return "ExcessiveAttemptsException" ; } catch (AuthenticationException e) { log.error("对用户[{}]进行登录验证,验证未通过,堆栈轨迹如下" , username, e); token.clear(); return "AuthenticationException" ; } return "success" ; } }
UserController 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 package com.battcn.controller;import org.apache.shiro.authz.annotation.Logical;import org.apache.shiro.authz.annotation.RequiresRoles;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping ("/users" )public class UserController { @GetMapping public String get () { return "get....." ; } @RequiresRoles (value = {"admin" , "test" }, logical = Logical.OR) @GetMapping ("/query" ) public String query () { return "query....." ; } @GetMapping ("/find" ) public String find () { return "find....." ; } }
主函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.battcn;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication public class Chapter25Application { public static void main (String[] args) { SpringApplication.run(Chapter25Application.class, args); } }
测试 启动 Chapter25Application.java
中的 main
方法,为了更好的演示效果这里打开了 postman
做的测试,只演示其中一个流程,剩下的可以自己复制代码测试…
先登录,由于 u3
在 DBCache
中拥有的角色是 test
,只有 user:list
这一个权限
访问 /users/query
成功,因为我们符合响应的角色/权限
访问 /users/find
失败,并重定向到了 /denied
接口,问题来了为什么 /users/find
没有写注解也权限不足呢?
细心的朋友肯定会发现 在 ShiroConfiguration 中写了一句 permissions.put(“/users/find”, “perms[user:find]”); 意味着我们不仅可以通过注解方式,同样可以通过初始化时加载数据库中的权限树做控制,看各位喜好了….
总结 目前很多大佬都写过关于 SpringBoot
的教程了,如有雷同,请多多包涵,本教程基于最新的 spring-boot-starter-parent:2.0.3.RELEASE
编写,包括新版本的特性都会一起介绍…
说点什么
个人QQ:1837307557
battcn开源群(适合新手):391619659
微信公众号(欢迎调戏):battcn
个人博客:http://blog.battcn.com/
全文代码:https://github.com/battcn/spring-boot2-learning/tree/master/chapter25