导读:
在阅读这篇文章之前假设你已经对Apache Shiro(后面统一用Shiro作为代指)有了一定的了解,如果你还对Shiro不熟悉的话在这篇文章的结尾附有相关的学习资料,关于Shiro是用来做什么的这里有个不错的介绍,在后面的文章中就不在对其进行描述了。后面的文章将围绕着 Spring Boot 集成Shiro 来进行展开。
快速上手:
1.引入pom依赖
<!-- 使用Shiro认证 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.5</version>
</dependency>
2.实现相关的 用户,角色,权限等代码的编写:
由于篇幅原因这里不进行展开 提供一个参考
3.实现Realm:
AbstractUserRealm继承AuthorizingRealm,并重写doGetAuthorizationInfo(用于获取认证成功后的角色、权限等信息) 和 doGetAuthenticationInfo(验证当前登录的Subject)方法:
public abstract class AbstractUserRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(AbstractUserRealm.class);
@Autowired
private UserRepository userRepository;
//获取用户组的权限信息
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 = userRepository.findByLoginName(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 = userRepository.findByLoginName(token.getUsername());
if (user != null) {
// 若存在,将此用户存放到登录认证info中,无需自己做密码对比,Shiro会为我们进行密码对比校验
return new SimpleAuthenticationInfo(user.getLoginName(), user.getPassword(), 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;
}
}
@Component
public class UserRealm extends AbstractUserRealm {
@Override
public UserRolesAndPermissions doGetGroupAuthorizationInfo(User userInfo) {
Set<String> userRoles = new HashSet<>();
Set<String> userPermissions = new HashSet<>();
//TODO 获取当前用户下拥有的所有角色列表,及权限
return new UserRolesAndPermissions(userRoles, userPermissions);
}
@Override
public UserRolesAndPermissions doGetRoleAuthorizationInfo(User userInfo) {
Set<String> userRoles = new HashSet<>();
Set<String> userPermissions = new HashSet<>();
//TODO 获取当前用户下拥有的所有角色列表,及权限
return new UserRolesAndPermissions(userRoles, userPermissions);
}
}
4.创建Shiro配置类:
这是最重要的一步等价于常规的Spring web应用的配置文件,将相关的配置托管给Spring 管理。
@Configuration
public class ShiroConfiguration {
private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);
/**
* Shiro的Web过滤器Factory 命名:shiroFilter<br /> * * @param securityManager * @return
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
logger.info("注入Shiro的Web过滤器-->shiroFilter", ShiroFilterFactoryBean.class);
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//Shiro的核心安全接口,这个属性是必须的
shiroFilterFactoryBean.setSecurityManager(securityManager);
//要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
//登录成功后要跳转的连接,逻辑也可以自定义,例如返回上次请求的页面
shiroFilterFactoryBean.setSuccessUrl("/index");
//用户访问未对其授权的资源时,所显示的连接
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
/*定义shiro过滤器,例如实现自定义的FormAuthenticationFilter,需要继承FormAuthenticationFilter **本例中暂不自定义实现,在下一节实现验证码的例子中体现 */
/*定义shiro过滤链 Map结构 * Map中key(xml中是指value值)的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的 * anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种 * authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter */
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
// <!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/login", "anon");//anon 可以理解为不拦截
filterChainDefinitionMap.put("/reg", "anon");
filterChainDefinitionMap.put("/plugins/**", "anon");
filterChainDefinitionMap.put("/pages/**", "anon");
filterChainDefinitionMap.put("/api/**", "anon");
filterChainDefinitionMap.put("/dists/img/*", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
return cacheManager;
}
/**
* 不指定名字的话,自动创建一个方法名第一个字母小写的bean * @Bean(name = "securityManager") * @return
*/
@Bean
public SecurityManager securityManager(UserRealm userRealm) {
logger.info("注入Shiro的Web过滤器-->securityManager", ShiroFilterFactoryBean.class);
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
//注入缓存管理器;
securityManager.setCacheManager(ehCacheManager());//这个如果执行多次,也是同样的一个对象;
return securityManager;
}
/**
* Shiro生命周期处理器 * @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能 * @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;
}
}
5.实现登录/退出等操作:
@Controller
public class SecurityController {
private static final Logger logger = LoggerFactory.getLogger(SecurityController.class);
@Autowired
private UserService userService;
@GetMapping("/login")
public String loginForm() {
return "login";
}
@PostMapping("/login")
public String login(@Valid User user, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "login";
}
String loginName = user.getLoginName();
logger.info("准备登陆用户 => {}", loginName);
UsernamePasswordToken token = new UsernamePasswordToken(loginName,user.getPassword());
//获取当前的Subject
Subject currentUser = SecurityUtils.getSubject();
try {
//在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查
//每个Realm都能在必要时对提交的AuthenticationTokens作出反应
//所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法
logger.info("对用户[" + loginName + "]进行登录验证..验证开始");
currentUser.login(token);
logger.info("对用户[" + loginName + "]进行登录验证..验证通过");
} catch (UnknownAccountException uae) {
logger.info("对用户[" + loginName + "]进行登录验证..验证未通过,未知账户");
redirectAttributes.addFlashAttribute("message", "未知账户");
} catch (IncorrectCredentialsException ice) {
logger.info("对用户[" + loginName + "]进行登录验证..验证未通过,错误的凭证");
redirectAttributes.addFlashAttribute("message", "密码不正确");
} catch (LockedAccountException lae) {
logger.info("对用户[" + loginName + "]进行登录验证..验证未通过,账户已锁定");
redirectAttributes.addFlashAttribute("message", "账户已锁定");
} catch (ExcessiveAttemptsException eae) {
logger.info("对用户[" + loginName + "]进行登录验证..验证未通过,错误次数过多");
redirectAttributes.addFlashAttribute("message", "用户名或密码错误次数过多");
} catch (AuthenticationException ae) {
//通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景
logger.info("对用户[" + loginName + "]进行登录验证..验证未通过,堆栈轨迹如下");
ae.printStackTrace();
redirectAttributes.addFlashAttribute("message", "用户名或密码不正确");
}
//验证是否登录成功
if (currentUser.isAuthenticated()) {
logger.info("用户[" + loginName + "]登录认证通过(这里可以进行一些认证通过后的一些系统参数初始化操作)");
return "redirect:/index";
} else {
token.clear();
return "redirect:/login";
}
}
@GetMapping("/logout")
public String logout(RedirectAttributes redirectAttributes) {
//使用权限管理工具进行用户的退出,跳出登录,给出提示信息
SecurityUtils.getSubject().logout();
redirectAttributes.addFlashAttribute("message", "您已安全退出");
return "redirect:/login";
}
@GetMapping("/reg")
@ResponseBody
public Result<String> reg(@Valid User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return Result.error("用户信息填写不完整");
}
userService.save(user);
return Result.ok();
}
}
6.前端页面编写:
一个简单 form表单提交的demo
<form action="login" method="POST">
<div class="form-group has-feedback">
<input name="loginName" type="text" class="form-control" placeholder="用户名"/>
<span class="glyphicon glyphicon-user form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input name="password" type="password" class="form-control"/>
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="row">
<!-- /.col -->
<div class="col-xs-12">
<button type="submit" class="btn btn-primary btn-block btn-flat">登 录</button>
</div>
<!-- /.col -->
</div>
</form>
扩展:
权限注解:
@RequiresAuthentication
表示当前Subject已经通过login进行了身份验证;即Subject. isAuthenticated()返回true。
@RequiresUser
表示当前Subject已经身份验证或者通过记住我登录的。
@RequiresGuest
表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)
表示当前Subject需要角色admin和user。
@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)
表示当前Subject需要权限user:a或user:b。
标签
代码验证:
(暂时忽略)留待补充
结语:
Shiro 作为一款安全框架为我们提供了常用的功能,已经足够应对绝大多数的业务需要,在下一篇文章中将介绍一款更加强大的安全框架 Spring Security。
参考资料:
Spring Boot系列(十五) 安全框架Apache Shiro(一)基本功能
Spring Boot Shiro 权限管理
学习资料:
Apache Shiro 使用手册
《跟开涛学Shiro》 – 博客版
跟开涛学 Shiro – wiki版
官方文档