Spring boot使用Shiro框架
1.maven引包
1 <dependency> 2 <groupId>org.apache.shiro</groupId> 3 <artifactId>shiro-spring</artifactId> 4 <version>1.2.5</version> 5 </dependency> 6 <dependency> 7 <groupId>org.apache.shiro</groupId> 8 <artifactId>shiro-ehcache</artifactId> 9 <version>1.2.5</version> 10 </dependency>
2.创建数据库
shiro一般需要5张表:3张实体表user,role,permission(一对一),2张关系映射表user-role,role-permission(一对多) 大致结构:
user:id name password ...
role : id name
permission:id name
user-role:uid rid
role-permission:rid pid
配置文件和model,mapper,service等构建过程不赘述,能实现查询数据库即可
3.创建抽象类AbstractUserRealm
package com.tqh.demo.config; import com.tqh.demo.model.User; import com.tqh.demo.service.UserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationException; 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 java.util.HashSet; import java.util.Set; public abstract class AbstractUserRealm extends AuthorizingRealm { private static final Logger logger = LoggerFactory.getLogger(AbstractUserRealm.class); @Autowired private UserService userService; //获取用户组的权限信息 public abstract UserRolesAndPermissions doGetGroupAuthorizationInfo(User userInfo); //获取用户角色的权限信息 public abstract UserRolesAndPermissions doGetRoleAuthorizationInfo(User userInfo); /** * 获取授权信息 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String currentLoginName = (String) principals.getPrimaryPrincipal(); Set<String> userRoles = new HashSet<>(); Set<String> userPermissions = new HashSet<>(); //从数据库中获取当前登录用户的详细信息 User userInfo = userService.findUserByName(currentLoginName); if (null != userInfo) { UserRolesAndPermissions groupContainer = doGetGroupAuthorizationInfo(userInfo); UserRolesAndPermissions roleContainer = doGetGroupAuthorizationInfo(userInfo); userRoles.addAll(groupContainer.getUserRoles()); userRoles.addAll(roleContainer.getUserRoles()); userPermissions.addAll(groupContainer.getUserPermissions()); userPermissions.addAll(roleContainer.getUserPermissions()); } else { throw new AuthorizationException(); } //为当前用户设置角色和权限 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addRoles(userRoles); authorizationInfo.addStringPermissions(userPermissions); logger.info("###【获取角色成功】[SessionId] => {}", SecurityUtils.getSubject().getSession().getId()); return authorizationInfo; } /** * 登录认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //UsernamePasswordToken对象用来存放提交的登录信息 UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; //查出是否有此用户 User user = userService.findUserByName(token.getUsername()); if (user != null) { // 若存在,将此用户存放到登录认证info中,无需自己做密码对比,Shiro会为我们进行密码对比校验 return new SimpleAuthenticationInfo(user.getNickname(), user.getPswd(), getName()); } return null; } protected class UserRolesAndPermissions { Set<String> userRoles; Set<String> userPermissions; public UserRolesAndPermissions(Set<String> userRoles, Set<String> userPermissions) { this.userRoles = userRoles; this.userPermissions = userPermissions; } public Set<String> getUserRoles() { return userRoles; } public Set<String> getUserPermissions() { return userPermissions; } } }
注:shiro通过Realm类实现用户的登录和权限验证
4.创建UserRealm实现上面的抽象类
1 package com.tqh.demo.config; 2 3 import com.tqh.demo.model.Permission; 4 import com.tqh.demo.model.Role; 5 import com.tqh.demo.model.User; 6 import com.tqh.demo.service.PermissionService; 7 import com.tqh.demo.service.RoleService; 8 import org.springframework.beans.factory.annotation.Autowired; 9 import org.springframework.stereotype.Component; 10 11 import java.util.HashSet; 12 import java.util.Set; 13 14 @Component 15 public class UserRealm extends AbstractUserRealm{ 16 17 @Autowired 18 RoleService roleService; 19 @Autowired 20 PermissionService permissionService; 21 @Override 22 public UserRolesAndPermissions doGetGroupAuthorizationInfo(User userInfo) { 23 Set<String> userRoles = new HashSet<>(); 24 Set<String> userPermissions = new HashSet<>(); 25 //获取当前用户下拥有的所有角色及权限 26 String[] roleIDs=roleService.findRolesByUser(userInfo.getId()); 27 for(String id:roleIDs){ 28 Role role=roleService.findRolebyID(id); 29 if(null!=role){ 30 userRoles.add(role.getName()); 31 //取出每个角色的所有权限 32 String[] permisionIDs =permissionService.findPermissionsByRole(role.getId()); 33 for(String pid:permisionIDs){ 34 Permission permission=permissionService.findPermissionByID(pid); 35 if(null!=permission){ 36 if(!userPermissions.contains(permission.getName())){ 37 userPermissions.add(permission.getName()); 38 } 39 } 40 } 41 } 42 } 43 return new UserRolesAndPermissions(userRoles, userPermissions); 44 } 45 46 @Override 47 public UserRolesAndPermissions doGetRoleAuthorizationInfo(User userInfo) { 48 return doGetGroupAuthorizationInfo(userInfo); 49 } 50 }
5.创建shiro的配置类ShiroConfiguration
1 package com.tqh.demo.config; 2 3 import org.apache.shiro.cache.ehcache.EhCacheManager; 4 import org.apache.shiro.spring.LifecycleBeanPostProcessor; 5 import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; 6 import org.apache.shiro.spring.web.ShiroFilterFactoryBean; 7 import org.apache.shiro.web.mgt.DefaultWebSecurityManager; 8 import org.slf4j.Logger; 9 import org.slf4j.LoggerFactory; 10 import org.apache.shiro.mgt.SecurityManager; 11 import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; 12 import org.springframework.context.annotation.Bean; 13 import org.springframework.context.annotation.Configuration; 14 import org.springframework.context.annotation.DependsOn; 15 16 import java.util.LinkedHashMap; 17 import java.util.Map; 18 19 @Configuration 20 public class ShiroConfiguration { 21 private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class); 22 23 /** 24 * Shiro的Web过滤器Factory 命名:shiroFilter<br /> * * @param securityManager * @return 25 */ 26 @Bean(name = "shiroFilter") 27 public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { 28 logger.info("注入Shiro的Web过滤器-->shiroFilter", ShiroFilterFactoryBean.class); 29 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); 30 31 //Shiro的核心安全接口,这个属性是必须的 32 shiroFilterFactoryBean.setSecurityManager(securityManager); 33 //要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 34 shiroFilterFactoryBean.setLoginUrl("/login"); 35 //登录成功后要跳转的连接,逻辑也可以自定义,例如返回上次请求的页面 36 shiroFilterFactoryBean.setSuccessUrl("/index"); 37 //用户访问未对其授权的资源时,所显示的连接, 使用perms,roles,ssl,rest,port等filter才有效,最好使用全局异常处理 38 shiroFilterFactoryBean.setUnauthorizedUrl("/403"); 39 /*定义shiro过滤器,例如实现自定义的FormAuthenticationFilter,需要继承FormAuthenticationFilter **本例中暂不自定义实现,在下一节实现验证码的例子中体现 */ 40 41 /*定义shiro过滤链 Map结构 * Map中key(xml中是指value值)的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的 * anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种 * authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter */ 42 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); 43 // 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了 44 filterChainDefinitionMap.put("/logout", "logout"); 45 46 // <!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了; 47 // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问--> 48 filterChainDefinitionMap.put("/login", "anon");//anon 可以理解为不拦截 49 filterChainDefinitionMap.put("/403", "anon"); 50 filterChainDefinitionMap.put("/reg", "anon"); 51 filterChainDefinitionMap.put("/plugins/**", "anon"); 52 filterChainDefinitionMap.put("/pages/**", "anon"); 53 filterChainDefinitionMap.put("/api/**", "anon"); 54 filterChainDefinitionMap.put("/dists/img/*", "anon"); 55 filterChainDefinitionMap.put("/**", "authc");//表示需要认证才可以访问 56 57 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); 58 59 return shiroFilterFactoryBean; 60 } 61 62 @Bean 63 public EhCacheManager ehCacheManager() { 64 EhCacheManager cacheManager = new EhCacheManager(); 65 return cacheManager; 66 } 67 68 /** 69 * 不指定名字的话,自动创建一个方法名第一个字母小写的bean * @Bean(name = "securityManager") * @return 70 */ 71 @Bean 72 public SecurityManager securityManager(UserRealm userRealm) { 73 logger.info("注入Shiro的Web过滤器-->securityManager", ShiroFilterFactoryBean.class); 74 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); 75 securityManager.setRealm(userRealm); 76 //注入缓存管理器; 77 securityManager.setCacheManager(ehCacheManager());//这个如果执行多次,也是同样的一个对象; 78 return securityManager; 79 } 80 81 /** 82 * Shiro生命周期处理器 * @return 83 */ 84 @Bean 85 public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { 86 return new LifecycleBeanPostProcessor(); 87 } 88 89 /** 90 * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能 * @return 91 */ 92 @Bean 93 @DependsOn({"lifecycleBeanPostProcessor"}) 94 public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { 95 DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); 96 advisorAutoProxyCreator.setProxyTargetClass(true); 97 return advisorAutoProxyCreator; 98 } 99 100 @Bean 101 public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { 102 AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); 103 authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); 104 return authorizationAttributeSourceAdvisor; 105 } 106 }
注:需要shiro实现的功能都在这里配置,比如remenber me等等,此处未添加
6.添加全局异常处理
1 package com.tqh.demo.exception; 2 3 import org.apache.shiro.authc.ExcessiveAttemptsException; 4 import org.apache.shiro.authc.IncorrectCredentialsException; 5 import org.apache.shiro.authc.LockedAccountException; 6 import org.apache.shiro.authc.UnknownAccountException; 7 import org.apache.shiro.authz.AuthorizationException; 8 import org.slf4j.Logger; 9 import org.slf4j.LoggerFactory; 10 import org.springframework.web.bind.annotation.ControllerAdvice; 11 import org.springframework.web.bind.annotation.ExceptionHandler; 12 13 14 /** 15 * @Author: Mcorleon 16 * @Date: 18-7-26 19:55 17 */ 18 @ControllerAdvice 19 public class GlobalExceptionHandler { 20 private final static Logger logger= LoggerFactory.getLogger(GlobalExceptionHandler.class); 21 22 @ExceptionHandler(AuthorizationException.class) 23 public String AuthenticationExceptionHandler(AuthorizationException e){ 24 logger.error("权限错误"); 25 logger.error(e.getMessage()); 26 return "403"; 27 } 28 29 @ExceptionHandler(UnknownAccountException.class) 30 public String AUnknownAccountExceptionHandler(UnknownAccountException e){ 31 logger.error("未知用户"); 32 logger.error(e.getMessage()); 33 return "Err"; 34 } 35 @ExceptionHandler(IncorrectCredentialsException.class) 36 public String IncorrectCredentialsExceptionHandler(IncorrectCredentialsException e){ 37 logger.error("校验出错"); 38 logger.error(e.getMessage()); 39 return "Err"; 40 } 41 @ExceptionHandler(LockedAccountException.class) 42 public String LockedAccountExceptionHandler(LockedAccountException e){ 43 logger.error("账户已锁定"); 44 logger.error(e.getMessage()); 45 return "Err"; 46 } 47 @ExceptionHandler(ExcessiveAttemptsException.class) 48 public String ExcessiveAttemptsExceptionHandler(ExcessiveAttemptsException e){ 49 logger.error("错误次数过多"); 50 logger.error(e.getMessage()); 51 return "Err"; 52 } 53 54 }
注: 出现AuthorizationException异常,也就是无权限访问时返回403前缀以进入”/403“控制器。
前面配置类里规定的shiroFilterFactoryBean.setUnauthorizedUrl("/403") 对使用perms,roles,ssl,rest,port等filter的链接才有效。
7.测试
Controller:
1 package com.tqh.demo.controller; 2 3 import org.apache.shiro.SecurityUtils; 4 import org.apache.shiro.authc.*; 5 import org.apache.shiro.authz.annotation.RequiresRoles; 6 import org.apache.shiro.subject.Subject; 7 import org.apache.shiro.web.util.SavedRequest; 8 import org.apache.shiro.web.util.WebUtils; 9 import org.slf4j.Logger; 10 import org.slf4j.LoggerFactory; 11 import org.springframework.stereotype.Controller; 12 import org.springframework.web.bind.annotation.GetMapping; 13 import org.springframework.web.bind.annotation.PostMapping; 14 import org.springframework.web.bind.annotation.RequestMapping; 15 16 import javax.servlet.http.HttpServletRequest; 17 18 @Controller 19 @RequestMapping("/") 20 public class MainController { 21 private static final Logger logger = LoggerFactory.getLogger(MainController.class); 22 @GetMapping("/login") 23 public String loginPage(){ 24 return "login"; 25 } 26 27 @PostMapping("/login") 28 public String login(String id , String psw, HttpServletRequest request) { 29 30 String loginName =id; 31 String password=psw; 32 logger.info("准备登陆用户 => {}", loginName); 33 UsernamePasswordToken token = new UsernamePasswordToken(loginName,password); 34 //获取当前的Subject 35 Subject currentUser = SecurityUtils.getSubject(); 36 //在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查 37 //每个Realm都能在必要时对提交的AuthenticationTokens作出反应 38 //所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法 39 logger.info("对用户[" + loginName + "]进行登录验证..验证开始"); 40 currentUser.login(token); 41 logger.info("对用户[" + loginName + "]进行登录验证..验证通过"); 42 43 //验证是否登录成功 44 if (currentUser.isAuthenticated()) { 45 logger.info("用户[" + loginName + "]登录认证通过"+" sessionID:"+currentUser.getSession().getId().toString()); 46 SavedRequest savedRequest = WebUtils.getSavedRequest(request); 47 // 获取保存的URL 48 if (savedRequest == null || savedRequest.getRequestUrl() == null) { 49 return "redirect:/index"; 50 } 51 //跳转到登陆之前的页面 52 return "redirect:" + savedRequest.getRequestUrl(); 53 54 } else { 55 token.clear(); 56 return "redirect:/login"; 57 } 58 } 59 60 @RequestMapping("/index") 61 public String loginSuccess(){ 62 return "index"; 63 } 64 65 66 @RequestMapping("/403") 67 public String accessFault(){ 68 return "403"; 69 } 70 71 @RequiresRoles("user") 72 @RequestMapping("/test") 73 public String accessTest(){ 74 return "test"; 75 } 76 }
新建一个简单的登录html即可测试,不赘述
subject的login()方法会触发Realm的doGetAuthenticationInfo()方法
添加了@RequiresRoles("user")标签就需要user角色才能访问,未登录会自动进入login页面,登陆成功配置了跳转入先前页面,触发Realm的doGetAuthorizationInfo方法检查权限,无权限则进入403页面