源码地址:https://github.com/q200737056/Spring-Course/tree/master/springboot2Shiro
一、项目环境
Java8+Maven3.3.9+SpringBoot2.0.4+Mybatis3+Shiro+H2+Eclipse
注意:使用了H2嵌入式内存模式的数据库
二、Shiro简介
Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。并且相对于其他安全框架,Shiro 要简单的多。
架构
- Subject:主体,即subject记录了当前操作的主体。可能是一个通过浏览器请求的用户,也可能是一个运行的程序。
- SecurityManager:安全管理器,它是shiro的核心。管理着认证,授权,session管理,缓存等。
- Authentication:认证器,对用户身份进行认证。
- Authorization:授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
- SessionManager:会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session。
- SessionDAO:会话dao,是对session会话操作的一套接口。
- CacheManager:缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
- Cryptography:密码管理,shiro提供了一套加密/解密的组件,保护数据的安全性,比如用户密码。
- Realm:域,相当于数据源,SecurityManager需要通过Realm获取用户权限数据。
三、SpringBoot整合Shiro
jar包依赖
<!--省略sringboot等其它依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
JavaConfig配置Shiro
@Configuration
public class ShiroConfig {
/**
* shiro过滤器
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//配置登录的url,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/index/loginIndex");
//未授权界面;配置不会被拦截的链接
//shiroFilterFactoryBean.setUnauthorizedUrl("/index/noperms");
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
filterChainDefinitionMap.put("/h2-console/**", "anon");
filterChainDefinitionMap.put("/index/login", "anon");
filterChainDefinitionMap.put("/index/noperms", "anon");
//filterChainDefinitionMap.put("/index/toAdd", "perms[user:add]");
//filterChainDefinitionMap.put("/index/**", "authc");
filterChainDefinitionMap.put("/index/logout", "logout");
//主要这行代码必须放在所有权限设置的最后,user拦截表示 用户存在或记住我 可以访问
filterChainDefinitionMap.put("/**", "user");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 配置核心安全管理器
* @param userRealm
* @return
*/
@Bean
public SecurityManager securityManager(UserRealm userRealm,
MemoryConstrainedCacheManager cacheManager) {
DefaultWebSecurityManager defaultSecurityManager = new
DefaultWebSecurityManager();
//设置 领域
defaultSecurityManager.setRealm(userRealm);
//设置 缓存
defaultSecurityManager.setCacheManager(cacheManager());
//设置 记住我
defaultSecurityManager.setRememberMeManager(rememberMeManager());
//设置 session管理器
defaultSecurityManager.setSessionManager(sessionManager());
return defaultSecurityManager;
}
/**
* 自定义凭证匹配器
*/
@Bean
public SimpleCredentialsMatcher customCredentialsMatcher(){
return new CustomCredentialsMatcher();
}
/**
* 自定义Realm
* @return
*/
@Bean
public UserRealm userRealm() {
UserRealm realm = new UserRealm();
realm.setCredentialsMatcher(customCredentialsMatcher());
return realm;
}
/**
* 使用shiro自带的缓存,当然还可以使用第三方缓存
* @return
*/
@Bean
public MemoryConstrainedCacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
/**
* 会话管理管理器
* @return
*/
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new
DefaultWebSessionManager();
//全局会话超时时间(单位毫秒),默认30分钟
sessionManager.setGlobalSessionTimeout(1800000);
sessionManager.setSessionDAO(sessionDAO());
//删除过期的session
sessionManager.setDeleteInvalidSessions(true);
//是否开启会话验证器,默认是开启的
sessionManager.setSessionValidationSchedulerEnabled(true);
//去掉URL地标后面的JSESSIONID
sessionManager.setSessionIdUrlRewritingEnabled(false);
//定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话
sessionManager.setSessionValidationInterval(1800000);
//sessionID是否保存到cookie中
sessionManager.setSessionIdCookieEnabled(true);
//sessionID Cookie
sessionManager.setSessionIdCookie(sessionIdCookie());
return sessionManager;
}
/**
* 会话DAO
* @return
*/
@Bean
public SessionDAO sessionDAO() {
MemorySessionDAO sessionDAO = new MemorySessionDAO();
return sessionDAO;
}
/**
* 记住我 管理器
* @return
*/
@Bean
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager manager = new CookieRememberMeManager();
//cookie加密 密钥 ,默认AES算法
manager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
manager.setCookie(rememberMeCookie());
return manager;
}
/**
* 自动登录 Cookie
* @return
*/
@Bean
public SimpleCookie rememberMeCookie() {
//构造方法的参数 是cookie的名称
SimpleCookie cookie = new SimpleCookie("rememberMe");
// 记住我cookie生效时间1天 ,单位秒
cookie.setHttpOnly(true);
cookie.setMaxAge(86400);
return cookie;
}
/**
* 会话Cookie 保存sessionID
* @return
*/
@Bean
public SimpleCookie sessionIdCookie() {
//构造方法的参数 是cookie的名称
SimpleCookie cookie = new SimpleCookie("sid");
cookie.setHttpOnly(true);
//关闭浏览器后 ,此cookie清除
cookie.setMaxAge(-1);
return cookie;
}
/**
* *
* 开启Shiro的注解
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
*
* 配置以下两个bean
* DefaultAdvisorAutoProxyCreator(可选,可以不用配置)
* AOP方法级权限检查,扫描上下文,寻找所有的Advistor(通知器),将这些
Advisor应用到所有符合切入点的Bean中。
* AuthorizationAttributeSourceAdvisor AOP方法级权限检查
* * @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new
DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor
authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* Shiro生命周期处理器
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
最主要的三个一定要配置。ShiroFilterFactoryBean:主要是配置一些URL与拦截器。默认主要的拦截器有anon(可匿名访问),authc(需要身份认证),roles[XX](需要拥有某些角色),perms[xx](需要拥有某些权限),logout(登出),user(需要存在用户或记住我)等。
SecurityManager :需要配置自定义Realm。
UserRealm :自定义Realm。继承AuthorizingRealm,实现了认证及授权。
注意:为了使用注解方式,更加简便设置角色权限访问。需要借助AuthorizationAttributeSourceAdvisor 类。
public class UserRealm extends AuthorizingRealm {
@Autowired
private IndexService indexService;
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
String username = (String) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> stringSet = new HashSet<>();
//这边直接赋权限了,正式项目中,从数据库查询该用户的角色(role),
//角色的权 限(perm)
if("admin".equals(username)){
stringSet.add("user:query");
stringSet.add("user:update");
stringSet.add("user:add");
stringSet.add("user:delete");
}else{
stringSet.add("user:query");
}
info.setStringPermissions(stringSet);
return info;
}
/**
* 身份认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
String userName = (String) token.getPrincipal();
//查询数据库
String userPwd = this.indexService.login(userName);
if (userPwd == null) {
throw new UnknownAccountException();//用户名不存在
}
return new SimpleAuthenticationInfo(userName, userPwd,getName());
}
}
实例中自定义了凭证匹配器。当然可以直接使用Shiro自带的HashedCredentialsMatcher注入一些必要的参数setHashAlgorithmName("md5") setHashIterations(2)
。
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token,
AuthenticationInfo info) {
UsernamePasswordToken utoken=(UsernamePasswordToken) token;
//获得用户输入的密码
String inPassword = new String(utoken.getPassword());
//获得数据库中的密码
String dbPassword=(String) info.getCredentials();
//进行密码的比对(可以采用加盐的方式去检验,这边直接明文匹配)
return this.equals(inPassword, dbPassword);
}
}
Controller
以下截取了一部分
/**
* 首页
* @return
*/
@RequestMapping()
public String index(HttpSession session){
Subject subject = SecurityUtils.getSubject();
String name = (String)subject.getPrincipal();
System.out.println("subject:"+name);
session.setAttribute("username", name);
//System.out.println("==="+subject.getSession().getAttribute("username"));
return "forward:/index/userList";
}
/**
* 登陆页面
* @return
*/
@RequestMapping(value="/loginIndex",method=RequestMethod.GET)
public String loginIndex(){
return "index";
}
/**
* 登陆
* @PostMapping相当于@RequestMapping(method=RequestMethod.POST)
* @return
*/
@PostMapping("/login")
public String login(String name,String password,String rememberMe
,ModelMap modelMap){
boolean booRememberMe = false;
if("true".equals(rememberMe)){
booRememberMe=true;
}
// System.out.println("===="+booRememberMe);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken=new
UsernamePasswordToken(name,password,booRememberMe);
try {
subject.login(usernamePasswordToken); //登录
} catch (UnknownAccountException e) {
modelMap.put("msg", "用户名不存在!");
return "index";
}catch (IncorrectCredentialsException ice) {
modelMap.put("msg", "密码不正确!");
return "index";
}catch (LockedAccountException lae) {
modelMap.put("msg", "账户已锁定!");
return "index";
} catch (ExcessiveAttemptsException eae) {
modelMap.put("msg", "密码错误次数过多!");
} catch (AuthenticationException ae) {
modelMap.put("msg", "用户名或密码不正确!");
return "index";
}
return "forward:/index";
}
/**
* 这边自定义logout
* shiro有默认logout,会自动清除相关信息,返回配置的登陆页面
* 用户登出
* @param
* @return
*/
@RequestMapping("/logout")
public String logout(ModelMap modelMap) {
Subject subject = SecurityUtils.getSubject();
subject.logout();
modelMap.put("msg","安全退出!");
return "index";
}
/**
* 列出 所有用户
* @param modelMap
* @return
*/
@RequestMapping("/userList")
public String userList(ModelMap modelMap){
List<User> userList = this.indexService.findUserList();
modelMap.put("userList", userList);
return "userList";
}
/**
* 查询用户
* @param id
* @return
* @RequiresPermissions:shiro权限配置
* 如果没有权限的用户操作,会报错
* 解决方法1.捕获异常,后续进行相关提示或操作。
* 解决方法2.配置拦截 比如 /index/queryUser=perms[user:query];
* ShiroFilterFactoryBean设置没权限时的url setUnauthorizedUrl("/index/noperms")
* 这里使用了方法1 全局异常捕获处理
*/
@RequiresPermissions("user:query")
@PostMapping("/queryUser")
public String queryUser(User user,ModelMap modelMap){
List<User> userList = this.indexService.queryUserBy(user);
modelMap.put("userList", userList);
return "userList";
}
登录页面
<body>
<form action="/index/login" method="post" >
<div style="width: 400px;margin:30px auto;">
用户名:<input type="text" name="name" value="admin"><br /><br />
密码:<input type="password" name="password" value="admin"><br /><br />
自动登录<input type="checkbox" name="rememberMe" value="true"/>
<br /><br />
<input type="submit" value="登录" />
<br />
<span th:text="${msg==null?'':msg}"></span>
</div>
</form>
</body>
实例中实现了Shiro记住我,自动登录功能。注意的是需要配置Cookie来保存登录用户名及密码。为了安全考虑需要配置加密算法及密钥,Shiro默认AES算法。实例中还使用了Base64编码过的密钥,隐藏了明文。
还有一个需要注意的是当认证过的用户,没有权限操作时,方法会报没有权限异常UnauthorizedException,解决方法有二种。
- 配置URL拦截,比如
/index/queryUser=perms[user:query]
,ShiroFilterFactoryBean设置没权限时跳转的URLsetUnauthorizedUrl("/index/noperms")
。 - 使用注解,捕获异常。
推荐使用第二种方式,比较灵活,简便。
@ControllerAdvice
public class DefaultExceptionHandler {
@ExceptionHandler({UnauthorizedException.class})
@ResponseStatus(HttpStatus.OK)
public ModelAndView handleUnauthenticatedException
(UnauthorizedException e) {
ModelAndView mv = new ModelAndView();
mv.addObject("exception", e);
mv.setViewName("noPerms");
return mv;
}
}