SpringBoot集成Shiro实现登录认证和权限管理
一、Shiro集成
依赖包(pom.xml):
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
配置文件:
ehcache-shiro.xml
<?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"
/>
<!-- 登录记录缓存锁定10分钟 -->
<cache
name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
配置类:
ShiroConfiguration.java
package com.info.config;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.session.mgt.SessionManager;
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.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
/**
* Shiro 配置
*/
@Configuration
public class ShiroConfiguration implements EnvironmentAware {
@Autowired
private Environment env;
// implements EnvironmentAware:解决加上LifecycleBeanPostProcessor方法后Environment为空问题
@Override
public void setEnvironment(Environment environment) {
this.env = environment;
}
/**
* 实例化SecurityManager,该类是shiro的核心类
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
// 用户授权/认证信息Cache, 采用EhCache 缓存
securityManager.setCacheManager(getEhCacheManager());
// 自定义session管理,前后端分离后不能从cookie中取数据
// securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 配置缓存
*/
@Bean
public EhCacheManager getEhCacheManager() {
EhCacheManager em = new EhCacheManager();
em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return em;
}
//自定义sessionManager
@Bean
public SessionManager sessionManager() {
MySessionManager mySessionManager = new MySessionManager();
// mySessionManager.setSessionDAO(redisSessionDAO());
return mySessionManager;
}
/**
* 配置Realm
*/
@Bean(name = "myShiroRealm")
public MyShiroRealm myShiroRealm() {
MyShiroRealm realm = new MyShiroRealm();
return realm;
}
/**
* 注册DelegatingFilterProxy(Shiro) 集成Shiro有2种方法:
* 1. 按这个方法自己组装一个FilterRegistrationBean(这种方法更为灵活,可以自己定义UrlPattern,
* 在项目使用中你可能会因为一些很但疼的问题最后采用它, 想使用它你可能需要看官网或者已经很了解Shiro的处理原理了)
* 2. 直接使用ShiroFilterFactoryBean(这种方法比较简单,其内部对ShiroFilter做了组装工作,无法自己定义UrlPattern,默认拦截 /*)
*/
// @SuppressWarnings({ "rawtypes", "unchecked" })
// @Bean
// public FilterRegistrationBean filterRegistrationBean() {
// FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
// filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
// // 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
// filterRegistration.addInitParameter("targetFilterLifecycle", "true");
// filterRegistration.setEnabled(true);
// filterRegistration.addUrlPatterns("/*");// 可以自己灵活的定义很多,避免一些根本不需要被Shiro处理的请求被包含进来
// return filterRegistration;
// }
/**
* 该类可以保证实现了org.apache.shiro.util.Initializable 接口的shiro对象的init或者是destory方法被自动调用,
* 而不用手动指定init-method或者是destory-method方法 注意:如果使用了该类,则不需要手动指定初始化方法和销毁方法,否则会出错
* 加上后 Environment 为 NUll
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 下面两个配置主要用来开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持
*/
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
daap.setProxyTargetClass(true);
return daap;
}
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
aasa.setSecurityManager(securityManager);
return aasa;
}
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的连接
// shiroFilterFactoryBean.setSuccessUrl("/index");
// 设置无权限访问页面
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
loadShiroFilterChain(shiroFilterFactoryBean);
return shiroFilterFactoryBean;
}
/**
* 加载shiroFilter权限控制规则(从数据库读取然后配置)
*/
private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
// 下面这些规则配置最好配置到配置文件中
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
try {
List<String> anons = Arrays.asList(env.getProperty("anon").split(","));
System.out.println(anons.toString());
} catch (NullPointerException e) {
}
// 2.不拦截的请求
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
// 此处将logout页面设置为anon,而不是logout,因为logout被单点处理,而不需要再被shiro的logoutFilter进行拦截
filterChainDefinitionMap.put("/logout", "anon");
filterChainDefinitionMap.put("/error", "anon");
// 3.拦截的请求(从本地数据库获取或者从casserver获取(webservice,http等远程方式),看你的角色权限配置在哪里)
// 需要有add权限才能访问
// filterChainDefinitionMap.put("/index", "perms[add]");
// 需要是admin角色才能访问
filterChainDefinitionMap.put("/index", "roles[admin]");
// 4.登录过的不拦截
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
}
}
MyShiroRealm.java
package com.info.config;
import java.util.List;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.info.dao.UserDao;
import com.info.entity.Role;
import com.info.entity.User;
public class MyShiroRealm extends AuthorizingRealm {
private static final Logger LOG = LoggerFactory.getLogger(MyShiroRealm.class);
@Autowired
private UserDao userDao;
/**
* 此方法调用 hasRole, hasPermission 的时候才会进行回调
*
* 权限信息(授权): 1、如果用户正常退出,缓存自动清空; 2、如果用户非正常退出,缓存自动清空;
* 3、如果我们修改了用户的权限,而用户不退出系统,修改的权限无法立即生效。 (需要手动编程进行实现;放在service进行调用)
* 在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例, 调用clearCached方法;
* Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
*
* 权限认证,为当前登录的Subject授予角色和权限 经测试:本例中该方法的调用时机为需授权资源被访问时
* 经测试:并且每次访问需授权资源时都会执行该方法中的逻辑,这表明本例中默认并未启用AuthorizationCache
* 经测试:如果连续访问同一个URL(比如刷新),该方法不会被重复调用,Shiro有一个时间间隔(也就是cache时间,在ehcache-shiro.xml中配置),超过这个时间间隔再刷新页面,该方法会被执行
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
LOG.info("################## 执行Shiro权限认证 ##################");
// 获取当前登录输入的用户名,等价于(String) principalCollection.fromRealm(getName()).iterator().next();
String loginName = (String) super.getAvailablePrincipal(principalCollection);
// 到数据库查是否有此对象
// 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
User user=userDao.findByCode(loginName);
if (user != null) {
// 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 用户的角色集合
info.setRoles(user.getRolesName());
// 用户的角色对应的所有权限,如果只使用角色定义访问权限,下面的四行可以不要
// List<Role> roleList = user.getRoleList();
// for (Role role : roleList) {
// info.addStringPermissions(role.getPermissionsName());
// }
// 或者按下面这样添加添加一个角色, 不是配置意义上的添加, 而是证明该用户拥有admin角色
// simpleAuthorInfo.addRole("admin");
// 添加权限
// simpleAuthorInfo.addStringPermission("admin:manage");
return info;
}
// 返回null的话,就会导致任何用户访问被拦截的请求时,都会自动跳转到 unauthorizedUrl 指定的地址
return null;
}
/**
* 登录认证
* 判断当前登录用户是否正确,如果使用CAS来实现单点登录就不需要在写
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// UsernamePasswordToken:对象用来存放提交的登录信息
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
LOG.info("验证当前Subject时获取到token为:" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE));
// 判断用户是否存在
User user = userDao.findByCode(token.getUsername());
if (user != null) {
// 若存在,将此用户存放到登录认证info中,无需自己做密码对比,Shiro会为我们进行密码对比校验
return new SimpleAuthenticationInfo(user.getCode(), user.getPassword(), getName());
}
return null;
}
}
MySessionManager.java
前后端分离框架用到
package com.info.config;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* 自定义sessionId获取
* 传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,在前后端分离的项目中(也可在移动APP项目使用),
* 我们选择在ajax的请求头中传递sessionId,因此需要重写shiro获取sessionId的方式。
* 自定义MySessionManager类继承DefaultWebSessionManager类,重写getSessionId方法,代码如下
*/
public class MySessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
System.out.println("id = " + id);
// 如果请求头中有 Authorization 则其值为sessionId
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
// 否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
二、代码实现
DAO:
package com.info.dao;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.info.entity.User;
@Repository
public interface UserDao extends CrudRepository<User, Long> {
User findByCode(String code);
}
Controller:
package com.info.controller;
import java.util.Map;
import javax.validation.Valid;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.info.entity.User;
@Controller
public class ShiroController {
private static final Logger LOG = LoggerFactory.getLogger(ShiroController.class);
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String loginForm(Model model) {
return "login";
}
// 登录验证
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(@Valid User user, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "login";
}
UsernamePasswordToken token = new UsernamePasswordToken(user.getCode(), user.getPassword());
// 获取当前的 Subject
Subject subject = SecurityUtils.getSubject();
try {
// 在调用了login 方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查
// 每个Realm都能在必要时对提交的AuthenticationTokens作出反应
// 所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法
LOG.info("进行登录验证 ===> 验证开始");
subject.login(token);
LOG.info("进行登录验证 ===> 验证通过");
} catch (UnknownAccountException uae) {
LOG.info("进行登录验证 ===> 验证未通过,未知账户");
redirectAttributes.addFlashAttribute("message", "未知账户");
} catch (IncorrectCredentialsException ice) {
LOG.info("进行登录验证 ===> 验证未通过,错误的凭证");
redirectAttributes.addFlashAttribute("message", "密码不正确");
} catch (LockedAccountException lae) {
LOG.info("进行登录验证 ===> 验证未通过,账户已锁定");
redirectAttributes.addFlashAttribute("message", "账户已锁定");
} catch (ExcessiveAttemptsException eae) {
LOG.info("进行登录验证 ===> 验证未通过,错误次数过多");
redirectAttributes.addFlashAttribute("message", "用户名或密码错误次数过多");
} catch (AuthenticationException ae) {
// 通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景
LOG.info("进行登录验证 ===> 验证未通过,堆栈轨迹如下");
ae.printStackTrace();
redirectAttributes.addFlashAttribute("message", "用户名或密码不正确");
}
// 验证是否登录成功
if (subject.isAuthenticated()) {
LOG.info("进行登录验证 ===> 登录认证通过(这里可以进行一些认证通过后的一些系统参数初始化操作)");
return "redirect:/index";
} else {
token.clear();
return "redirect:/login";
}
}
}