首先,先上一个 Demo :
Demo 链接 :ShiroDemo
pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
// Spring MVC 项目
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
// 这里我们将 Shiro 的部分缓存放到 Redis 中
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
// 引入 shiro-spring 依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
</dependencies>
然后,我们创建一个 ShiroConfig 类:
ShiroConfig.class
@Component
public class ShiroConfig {
// Spring 集成 Shiro 时,通过 ShiroFilterFactoryBean 来实现
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置默认的 安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置登录页
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 设置登陆成功后的跳转页面
shiroFilterFactoryBean.setSuccessUrl("/index.html");
// 设置未授权页面
shiroFilterFactoryBean.setUnauthorizedUrl("/error.html");
// 添加 拦截器链
Map<String, String> filterChainMap = new LinkedHashMap<>();
filterChainMap.put("/login.html", "anon");
filterChainMap.put("/login", "anon");
filterChainMap.put("/error.html", "anon");
filterChainMap.put("/*", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
return shiroFilterFactoryBean;
}
// 声明 默认的 安全管理器 Bean,这里我们使用 DefaultWebSecurityManager
@Bean(name = "securityManager")
public SecurityManager securityManager(Realm realm,SessionManager sessionManager) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// Shiro 中进行认证与授权的操作是通过我们自定义的 Realm 接口的实现来完成的
defaultWebSecurityManager.setRealm(realm);
// 因为我们会将 session 信息缓存到 Redis 中,所以,我们需要自定义一个 Session Bean
defaultWebSecurityManager.setSessionManager(sessionManager);
return defaultWebSecurityManager;
}
// 声明一个 Session 管理器 Bean,Session 管理器底层是通过 SessionDAO 来进行 CRUD 操作,所以,我们声明的 Session 管理器中,需要传入我们自定义的 SessionDAO
@Bean
public SessionManager sessionManager(SessionDAO sessionDAO) {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setSessionDAO(sessionDAO);
return defaultWebSessionManager;
}
// 将我们自定义的 RedisSessionDAO 声明为一个 Bean
@Bean
public SessionDAO sessionDAO(RedisTemplate redisTemplate) {
RedisSessionDAO sessionDAO = new RedisSessionDAO();
sessionDAO.setRedisTemplate(redisTemplate);
return sessionDAO;
}
}
MyRealm.class
@Component
public class MyRealm extends AuthorizingRealm {
@Autowired
RedisTemplate redisTemplate;
//授权登录用户
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
throw new AuthorizationException("principals should not be null");
}
User user = (User) principals.getPrimaryPrincipal();
Object userJson = redisTemplate.opsForValue().get(user.getUserName());
User object = JSONUtil.getObject(userJson.toString(), User.class);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setRoles(object.getRoles());
return simpleAuthorizationInfo;
}
// 用户登录认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
User user = (User)redisTemplate.opsForValue().get(usernamePasswordToken.getUsername());
if (user == null) {
throw new UnknownAccountException();
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
usernamePasswordToken.getUsername(),
user.getPassWord(),
null,
getName()
);
return authenticationInfo;
}
}
RedisSessionDAO.class
public class RedisSessionDAO extends AbstractSessionDAO {
private final static Logger log = LoggerFactory.getLogger(RedisSessionDAO.class);
RedisTemplate redisTemplate;
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
private final static String SHIRO_SESSION_PREFIX = "shiro-session:";
@Override
protected Serializable doCreate(Session session) {
Serializable serializable = generateSessionId(session);
assignSessionId(session, serializable);
redisTemplate.opsForValue().set(SHIRO_SESSION_PREFIX + session.getId(), session);
setExpire(SHIRO_SESSION_PREFIX + session.getId(), null);
return serializable;
}
@Override
protected Session doReadSession(Serializable sessionId) {
SimpleSession json = (SimpleSession) redisTemplate.opsForValue().get(SHIRO_SESSION_PREFIX + sessionId);
Session session = null;
if (json != null) {
session = json;
}
return session;
}
@Override
public void update(Session session) throws UnknownSessionException {
SimpleSession json = (SimpleSession) redisTemplate.opsForValue().get(SHIRO_SESSION_PREFIX + session.getId());
if (json != null) {
redisTemplate.opsForValue().set(SHIRO_SESSION_PREFIX + session.getId(), session);
setExpire(SHIRO_SESSION_PREFIX + session.getId(), null);
} else {
throw new UnknownSessionException("not find sessionId : " + session.getId().toString() + " ");
}
}
@Override
public void delete(Session session) {
redisTemplate.delete(SHIRO_SESSION_PREFIX + session.getId());
log.info("delete session success;sessionId: {}", session.getId());
}
@Override
public Collection<Session> getActiveSessions() {
Set<String> keys = redisTemplate.keys(SHIRO_SESSION_PREFIX);
List<Session> list = new LinkedList<>();
if (CollectionUtils.isEmpty(keys)) {
return list;
}
Iterator<String> iterator = keys.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
SimpleSession json = (SimpleSession) redisTemplate.opsForValue().get(next);
list.add(json);
}
return list;
}
private void setExpire(String keyName, Integer timeOut) {
if (timeOut == null){
timeOut = 60 * 30;
}
redisTemplate.expire(keyName, timeOut, TimeUnit.SECONDS);
}
}
登录 Controller
LoginController.class
@Controller
@RequestMapping("/")
public class LoginController {
@RequestMapping(value = "login" ,method = RequestMethod.POST)
@ResponseBody
public String login(User user) {
//添加用户认证信息
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
user.getUserName(), user.getPassWord()
);
try {
//进行验证,这里可以捕获异常,然后返回对应信息
subject.login(usernamePasswordToken);
} catch (ShiroException e) {
e.printStackTrace();
return "fail";
}
return "success";
}
@RequestMapping(value = "logout",method = RequestMethod.GET)
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "/logout.html";
}
}
之后,便可以进行使用了,使用时,需现在 Redis 中缓存一条用户信息,因为我们这里是 MyRealm 中登录认证和授权时的操作都是通过去 Redis 中获取拿到的。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class LoginControllerTest {
private User user;
@Autowired
RedisTemplate redisTemplate;
@Before
public void before() {
user = new User();
user.setUserName("sunwukong");
user.setPassWord("123456");
user.setId(ShiroUtil.UUID());
Set<String> hashSet = new HashSet<>();
// 这里我设置了两个 角色 admin 和 user
hashSet.add("admin");
hashSet.add("user");
user.setRoles(hashSet);
redisTemplate.opsForValue().set(user.getUserName(), user);
}
@Test
public void index() {
System.out.println("123");
}
}
然后,便可以进行登录了。
下面来一起看一下 Shiro 运行的原理:
上面这张图,大家应该都很熟悉, 每个客户端进行操作时,对于 Shiro 来说,都是一个 Subject 主体,当请求发到后端后,Shiro 通过 SecurityManager 来对 Subject 主体 进行认证、授权等操作。通过实现自定义的 Realm 来实现对认证、授权信息的自定义操作。 对于新人,只需要先记住这些,剩下的 SessionManager 、CacheManager、Cryptography 等对 Shiro 的整个流程有了基本理解后,再来看会很容易明白。
我们已经知道,Shiro 是通过 SecurityManager 来对每一个请求的 Subject 来进行认证等处理,所以,我们来关注 SecurityManager ;Shiro 中,SecurityManager 的继承结构如下:
一般的默认实现是:DefaultSecurityManager 这个类,由于我们是 Web 程序,所以,在创建 SecurityManager Bean 时,我们使用 DefaultWebSecurityManager 作为默认实现,然后,关键的地方是:我们需要自定义认证、授权方式,所以,我们需要实现自定义的 Realm ,然后将其放到 SecurityManager Bean 中;然后我们来看 Realm 接口的实现:
其中 AuthenticatingRealm 和 AuthorizingRealm 这两个抽象类为登录认证、授权 的两个组件,所以,如果要实现,自定义的 Realm ,我们需要继承 AuthorizingRealm 这个抽象类,然后实现对应的 doGetAuthorizationInfo(…) 授权、doGetAuthenticationInfo(…) 认证 两个方法。
配置完 Realm 后,下面来看一下 Shiro 的过滤器链,过滤器链的作用是为了实现对后台项目资源根据权限或者认证来进行展示或者部分展示设置,首先,来看一下 Shiro 的过滤器类的继承结构图:
Shiro 中 提供的默认的各级别使用的拦截器在 DefaultFilter 这个类中可以找到:
DefaultFilter.class
public enum DefaultFilter {
// 不用认证,开放资源,使用该拦截器
anon(AnonymousFilter.class),
// 登录认证后的用户才可以访问的资源
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
authcBearer(BearerHttpAuthenticationFilter.class),
// 退出时的拦截器
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
// 权限拦截器
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
// HTTP 方法级别权限拦截器
rest(HttpMethodPermissionFilter.class),
// 角色拦截器
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);
...
}
上图为 shiro-web 包下提供的各级别拦截器的默认实现类,通过这些默认拦截器与 上图中的 Filter 类继承结构图可以看出,进行权限过滤的一些过滤器实现,大多是继承自 PathMatchingFilter;这是因为:Web 资源的访问是基于路径来实现的,所以,在访问 Web 资源时,Shiro 的过滤器会进行路径解析,当访问路径资源的 Subject 用户与我们配置的权限级别相匹配时,放行,否则,不允许用户访问。
在 Shiro 与 Spring 框架集成时,是通过 ShiroFilterFactoryBean 这个类来完成对 Shiro 的一系列配置,当我们在声明 Bean 时完成一些基础配置后,最终,是通过 SpringShiroFilter 来进行具体的拦截与放行。
ShiroFilterFactoryBean.class
protected AbstractShiroFilter createInstance() throws Exception {
// 获取到我们声明 ShiroFilterFactoryBean 时,向其中放入的 securityManager
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}
// 必须是 WebSecurityManager 类型;因为是 Web 项目
if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}
// 创建拦截器链管理器
FilterChainManager manager = createFilterChainManager();
// 将其放入 路径匹配过滤器链解析器中
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
// 将 securityManager 和 chainResolver 放入 SpringShiroFilter 中,进行返回
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
在 Filter 类继承结构中可以看到:SpringShiroFilter 继承自 AbstractShiroFilter ;而所有的过滤行为都发生在 AbstractShiroFilter#doFilterInternal(…) 方法中,下面为 doFilterInternal(…) 方法的关键部分代码:
AbstractShiroFilter.class
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
...
// 创建一个 Subject
final Subject subject = createSubject(request, response);
//当前 Subject 执行过滤器链,进行过滤
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
// 注意这里
executeChain(request, response, chain);
return null;
}
});
....
}
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
throws IOException, ServletException {
// 获取到对应的过滤器链,根据 访问的路径与 创建 ShiroFilterFactoryBean 时,其中添加的 filterChainDefinitionMap 集合的 key 进行 匹配
FilterChain chain = getExecutionChain(request, response, origChain);
// 执行过滤
chain.doFilter(request, response);
}
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;
// 创建 ShiroFilterFactoryBean 时,其中添加的 filterChainDefinitionMap 与 filters 会被依次放到 DefaultFilterChainManager 类型对象中,该对象会被放到 PathMatchingFilterChainResolver 路径匹配过滤器链解析器中
// 这一步会获取到对应在创建 ShiroFilterFactoryBean 创建的 PathMatchingFilterChainResolver 对象
FilterChainResolver resolver = getFilterChainResolver();
...
// 根据请求的路径进行匹配,获取到对应的拦截器链
FilterChain resolved = resolver.getChain(request, response, origChain);
...
return chain;
}
PathMatchingFilterChainResolver.class
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
FilterChainManager filterChainManager = getFilterChainManager();
...
// 获取到请求的 URL 路径
String requestURI = getPathWithinApplication(request);
...
// 循环过滤器链集合 的 key
// 这里的 filterChainManager 对应的即是我们的 Demo 例子中声明 shiroFilterFactoryBean Bean 时,向其中添加的 filterChainDefinitionMap
// 即:
// 添加 拦截器链
// Map<String, String> filterChainMap = new LinkedHashMap<>();
// filterChainMap.put("/login.html", "anon");
// filterChainMap.put("/login", "anon");
// filterChainMap.put("/error.html", "anon");
// filterChainMap.put("/*", "authc");
// shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
for (String pathPattern : filterChainManager.getChainNames()) {
...
// key 与 请求的路径进行匹配
if (pathMatches(pathPattern, requestURI)) {
...
// 路径匹配,则执行对应的过滤
return filterChainManager.proxy(originalChain, pathPattern);
}
}
return null;
}
Shiro 进行权限过滤的流程大致就是这样,下面来看 SessionManager 的实现:
上面说到,securityManager 对象持有一个 sessionManager,sessionManager 持有一个 sessionDAO ;所有对 sessionManager 的操作,都委托给其持有的 sessionDAO 对象来完成,所以,我们在实际的使用中,只需要创建一个自定义的 sessionDAO 对象即可;上面Demo 中我们创建的基于 Redis 实现的 sessionDAO ,所有的 CRUD 操作都是对 Redis 进行的。
SessionManager 与 SessionDAO 关系如下:
在 Shiro 集成 Spring 项目中,我们使用的是 DefaultWebSessionManager ,其父类 DefaultSessionManager 中持有一个 sessionDAO 对象,所有的 CRUD 即通过它来完成。
Shiro 的大致执行流程如下,如果想要实现自定义的 Filter ,则需要将自定义 Fitter 实现对应的 Filter 接口,可参考上面 Filter 继承结构图;然后在声明 ShiroFilterFactoryBean Bean 时,设置其的 filters 属性即可:
ShiroFilterFactoryBean .class
private Map<String, Filter> filters;
然后在其后设置 filterChainDefinitionMap 时,put 时的 value 对应 filters 中对应 Filter 的 key,filterChainDefinitionMap 中 put 的 key 为 路径。然后,在自定义的 Filter 中根据过滤级别,实现对应的父类方法,这里不做详细介绍,有兴趣可根据上面提供的 Filter 继承结构图翻看对应类的源码。