在Web应用中安全问题同样不可忽视,所以Spring Framework体系中也为此提供了解决方案——Spring Security。但是由于其较为复杂,新人上手较为困难。所以这里我们来介绍另外一个简单易用的开源安全框架——Apache Shiro,并说明如何在SpringBoot中实现身份认证、权限授权

配置

Maven依赖

在Pom.xml添加Shiro Framework依赖

<!-- Shiro Framework -->
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-starter</artifactId><version>1.4.0</version>
</dependency><!-- 如果使用Shiro权限授权的注解,还需要添加Spring AOP依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId><version>2.1.4.RELEASE</version>
</dependency>

自定义令牌

相比较每次HTTP请求通过用户名、密码来进行身份认证,使用后端生成的Token则会更加安全、方便,为此我们需要先自定义一个基于Token的令牌以用于身份认证,如下所示

/*** 自定义令牌*/
public class CustomToken implements AuthenticationToken {// HTTP Request Heard token 字段名public static String TOKENHEARDNAME = "token";private String token;public CustomToken(String token) {this.token = token;}/*** 获取标识信息* @return*/@Overridepublic Object getPrincipal() {return token;}/*** 获取凭证信息* @return*/@Overridepublic Object getCredentials() {return token;}
}

自定义拦截器 Filter

为保证Web应用安全,不被非法访问攻击。我们还需要自定义一个拦截器Filter,其作用就是使得HTTP请求在到达指定的Contorller之前被拦截以便进行认证、授权等任务。这里我们通过继承AuthenticatingFilter定义了一个我们自己的拦截器CustomFilter。在拦截器中,如果我们发现请求头中包含token字段(携带令牌),则予以放行说明该请求可以进行认证、授权任务;反之则在拦截器中直接过滤掉该请求

/*** 自定义拦截器*/
public class CustomFilter extends AuthenticatingFilter {/*** 以供认证时,根据Http请求头构建一个基于Token的自定义令牌* @param servletRequest* @param servletResponse* @return*/@Overrideprotected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {String token = getTokenFromHeard(servletRequest);if( token == null ) {return null;}return new CustomToken(token);}/*** 判定该请求是否允许访问,true: 则拦截器直接放行该请求; false: 将继续调用onAccessDenied方法* @apiNote 1. 在url拦截规则中, 对于无需认证的url直接设置为anon(可匿名访问),故不会被该拦截器所拦截,* @apiNote 2. 这里该方法可直接返回false,将是否需要进行认证放在 onAccessDenied 方法中去实现* @param request* @param response* @param mappedValue* @return*/@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {return false;}/*** 如果请求头中不含 token, 则返回false,拦截该请求;* 否则调用 executeLogin 方法 以进行基于token的自定义令牌的认证* @param servletRequest* @param servletResponse* @return* @throws Exception*/@Overrideprotected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {String token = getTokenFromHeard(servletRequest);// 无token则拦截该请求if( token==null ) {HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse;httpServletResponse.setStatus(401);httpServletResponse.getWriter().write("Error: No Token Message");return false;}return executeLogin(servletRequest, servletResponse);   // 有token则进行认证}/*** 可选地,用于向浏览器返回认证失败时的响应信息* @param token* @param e* @param request* @param response* @return*/@Overrideprotected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {HttpServletResponse httpServletResponse = (HttpServletResponse)response;httpServletResponse.setStatus(401);try {httpServletResponse.getWriter().write( e.getMessage() );} catch (IOException e1) {}return false;}/*** 从 HTTP Request Heard 中提取 token 字段的数据* @param request* @return*/private String getTokenFromHeard(ServletRequest request) {return ((HttpServletRequest)request).getHeader(CustomToken.TOKENHEARDNAME);}
}

自定义Realm

在Shiro中有一个Realm的概念,通过它来可以完成认证、授权等任务的具体逻辑。一般地,我们需要根据自己的实际业务需求来实现它,换句话说,Realm就相当于是一个安全相关的Dao。这里我们通过继承AuthorizingRealm类实现了一个自定义的Realm类,我们需要重写父类的doGetAuthenticationInfo、doGetAuthorizationInfo方法来定义我们的身份认证、权限授权的具体逻辑。在前文中,我们定义了一个自定义令牌——CustomToken,所以这里通过重写supports方法来只使用我们自定义的令牌

/*** 自定义Realm类,用于实现认证、授权的判定逻辑*/
public class CustomRealm extends AuthorizingRealm {@Autowiredprivate UserTokenService userTokenService;@Autowiredprivate UserService userService;@Autowiredprivate RolePermService rolePermService;/*** 是否支持该类型的令牌* @param token* @return*/@Overridepublic boolean supports(AuthenticationToken token) {// 只支持基于token的自定义令牌return token instanceof CustomToken;}/*** 认证,失败直接抛出异常即可* @param authenticationToken* @return* @throws AuthenticationException*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {String token = (String)authenticationToken.getPrincipal();Map param = new HashMap();param.clear();param.put("token", token);List<UserToken> userTokenList = userTokenService.findList(param);// 从数据库中查询符合的用户Token记录只应该有一条if( userTokenList==null || userTokenList.size() != 1 ) {throw new AuthenticationException("Error: Invalid Token");}Integer userId = userTokenList.get(0).getUserId();// 用户主键不应无效if( userId==null || userId <=0) {throw new AuthenticationException("Error: Invalid Token");}// 根据主键查询用户信息User user = userService.findById(userId);if( user==null ) {throw new AuthenticationException("Error: Invalid Token");}// 根据HTTP请求头中的token查找到相关的用户,支持认证成功AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, token, getName());return authenticationInfo;}/*** 授权* @apiNote 只有认证通过后才会进行授权* @param principalCollection* @return*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {// 获取用户的角色、权限信息User user = (User) principalCollection.getPrimaryPrincipal();RolePerm rolePerm = rolePermService.findById( user.getRoleId() );// 用户的角色名称集合Set<String> roleNameSet = new HashSet<>();roleNameSet.add( rolePerm.getName() );// 用户的权限集合Set<String> permNameSet = new HashSet<>();permNameSet.add( rolePerm.getPerm() );SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();info.setRoles( roleNameSet );   // 设置角色信息info.setStringPermissions( permNameSet );   // 设置权限信息return info;}
}

配置Shiro

至此,我们已经把Shiro的相关组件——Token、Filter、Realm,均已经配置完成,现在只需将它们装配在一起即可。示例如下所示,除login登录接口(该接口接收用户名、密码信息来返回token)外,其他接口均需要先进行身份认证

/*** Shiro Framework Config 配置类*/
@Configuration
public class ShiroConfig {// 配置一个自定义的Realm类@Bean(name = "customRealm")public CustomRealm getCustomRealm() {return new CustomRealm();}// 配置安全管理器@Bean(name = "securityManager")public DefaultWebSecurityManager  securityManager() {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm( getCustomRealm() );return securityManager;}// 设置 Shiro 的url拦截规则、拦截器@Bean(name = "shiroFilter")public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 设置自定义的拦截器FilterMap<String, Filter> filters = new HashMap(2);filters.put("customFilter", new CustomFilter() );shiroFilterFactoryBean.setFilters(filters);// 设置 url 拦截规则, 拦截时将按定义顺序进行匹配,故 /** 规则应放在最后Map<String, String> urlFilterMap = new LinkedHashMap<>();// anon 表示该url可匿名访问urlFilterMap.put("/login", "anon");// customFilter 表示该url将由指定的拦截器 customFilter 进行拦截处理urlFilterMap.put("/**", "customFilter");shiroFilterFactoryBean.setFilterChainDefinitionMap(urlFilterMap);return shiroFilterFactoryBean;}/******************** 如果使用Shiro权限授权的注解,还需配置 Shiro 权限控制的注解通知 ********************/@Beanpublic LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}@Bean@DependsOn("lifecycleBeanPostProcessor")public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);return defaultAdvisorAutoProxyCreator;}//配置Shiro通知器@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);return authorizationAttributeSourceAdvisor;}/******************************************************************/
}

身份认证

所谓认证就如我们在CustomRealm的doGetAuthenticationInfo方法中的逻辑一样,即判断该请求携带的Token数据是否能查询到相关的用户。如果能则说明身份认证成功;反之则不能。这里问为认证测试提供了一个Controller

@RestController
@ResponseBody
public class ShiroAuthenTestController {@Autowiredprivate UserService userService;@Autowiredprivate UserTokenService userTokenService;@GetMapping("/login")public String login(String username, String password) {String msg = "登录失败";if( StrUtil.isBlank(username) || StrUtil.isBlank(password) ) {return msg;}User user = userService.findByLoginMsg(username, password);if( user==null || user.getId()<=0 ) {return msg;}Map param = new HashMap<>();param.put("userId", user.getId());List<UserToken> userTokens = userTokenService.findList(param);if( userTokens==null || userTokens.size()!=1 ) {return msg;}msg = "Token: " + userTokens.get(0).getToken();return msg;}/*** Shiro 认证测试 Controller* @return*/@GetMapping("/testAuthen")public String testAuthen() {// 获取登录者的信息Subject subject = SecurityUtils.getSubject();User user = (User) subject.getPrincipal();System.out.println("Login Msg: " + user);String msg = "test Shiro success";return msg;}
}

现在我们通过PostMan来进行测试

1. 请求login接口

通过登录接口分别获取到用户Aaron、Bob的token信息为abcdefg、kfc

2. 请求testAuthen接口

当我们不携带token、携带错误的token发起请求时,可以看到会导致认证失败,接口没有响应

当我们携带正确的token发起请求时,则可以看到认证成功,接口正常响应

权限控制

这里为了便于演示,我们建立了一个简单的用户-权限表,如下图所示

从中我们可以得知:

  • 用户Aaron,其Token为abcdefg,角色为user用户,权限为accessPerm1
  • 用户Bob,其Token为kfc,角色为root用户,权限为accessPerm2

基于角色的授权

正如我们在CustomRealm的doGetAuthorizationInfo方法中的所做的那样,我们在认证通过之后会为其添加角色、权限相关的信息。在Shiro中可以通过@RequiresRoles注解来方便地表示访问Controller需要的角色条件,这里我们提供了testAuthor1、testAuthor2两个请求接口,前者只能是user用户可以访问,而后者只能是root用户才可以访问

@RestController
@ResponseBody
public class ShiroAuthorTestController {/*** Shiro 授权测试 Controller 1* 只允许 User 用户访问* @return*/@GetMapping("/testAuthor1")@RequiresRoles("user")public String testAuthor1() {return "Msg: success access by User";}/*** Shiro 授权测试 Controller 2* 只允许 Root 用户访问*/@GetMapping("/testAuthor2")@RequiresRoles("root")public String testAuthor2() {return "Msg: success access by User";}
}

现在我们通过PostMan来进行测试testAuthor1接口:

  • 当我们不携带token、携带错误的token发起请求时,可以看到会同样导致认证失败,接口没有响应
  • 当Aaron访问时,由于其角色为user用户,故接口被正确响应
  • 当Bob访问时,虽然认证通过了,但是由于其角色为root用户,没有权限访问该接口,故该接口没有响应

基于权限的授权

在Shiro中也可以通过@RequiresPermissions注解来方便地表示访问Controller需要的权限条件,这里我们提供了testAuthor3、testAuthor4两个请求接口,前者只能是拥有accessPerm1权限的用户可以访问,而后者只能是拥有accessPerm2权限的用户可以访问

@RestController
@ResponseBody
public class ShiroAuthorTestController {/*** Shiro 授权测试 Controller 3* 必须具备指定权限 accessPerm1 的 用户才可以访问* @return*/@GetMapping("/testAuthor3")@RequiresPermissions("accessPerm1")public String testAuthor3() {return "Msg: success access by Perm [accessPerm1] ";}/*** Shiro 授权测试 Controller 4* 必须具备指定权限 accessPerm2 的 用户才可以访问* @return*/@GetMapping("/testAuthor4")@RequiresPermissions("accessPerm2")public String testAuthor4() {return "Msg: success access by Perm [accessPerm2] ";}
}

现在我们通过PostMan来进行测试testAuthor3接口:

  • 当Aaron访问时,由于其权限条件为accessPerm1,故接口被正确响应
  • 当Bob访问时,虽然认证通过了,但是由于其只有accessPerm2的权限条件,故该接口没有响应

shiro放行_Shiro在Spring Boot中的实践相关推荐

  1. Spring Boot 中关于 %2e 的 Trick

    作者 | Ruilin 来源 | http://rui0.cn/archives/1643 分享一个Spring Boot中关于%2e的小Trick. 先说结论,当Spring Boot版本在小于等于 ...

  2. Spring Boot中使用Spring Security进行安全控制

    我们在编写Web应用时,经常需要对页面做一些安全控制,比如:对于没有访问权限的用户需要转到登录表单页面.要实现访问控制的方法多种多样,可以通过Aop.拦截器实现,也可以通过框架实现(如:Apache ...

  3. Spring Boot中防表单重复提交以及拦截器登录检测

    目录 理论 演示 源码 理论 在用户登录后,如果按F5刷新会出现表单重复提交的问题,解决这个问题后,如果没有拦截器登录检测,就会造成,任意用户可以登录后台界面,所以要有拦截器登录检测. 相关的逻辑步骤 ...

  4. Spring Boot 中关于 %2e 的 坑,希望你不要遇到

    作者 | Ruilin 来源 | http://rui0.cn/archives/1643 分享一个Spring Boot中关于%2e的小Trick. 先说结论,当Spring Boot版本在小于等于 ...

  5. Spring Boot 中密码加密的两种姿势!

    先说一句:密码是无法解密的.大家也不要再问松哥微人事项目中的密码怎么解密了! 密码无法解密,还是为了确保系统安全.今天松哥就来和大家聊一聊,密码要如何处理,才能在最大程度上确保我们的系统安全. 本文是 ...

  6. 再谈Spring Boot中的乱码和编码问题

    编码算不上一个大问题,即使你什么都不管,也有很大的可能你不会遇到任何问题,因为大部分框架都有默认的编码配置,有很多是UTF-8,那么遇到中文乱码的机会很低,所以很多人也忽视了. Spring系列产品大 ...

  7. 【spring boot2】第8篇:spring boot 中的 servlet 容器及如何使用war包部署

    嵌入式 servlet 容器 在 spring boot 之前的web开发,我们都是把我们的应用部署到 Tomcat 等servelt容器,这些容器一般都会在我们的应用服务器上安装好环境,但是 spr ...

  8. Spring Boot 中使用 MongoDB 增删改查

    本文快速入门,MongoDB 结合SpringBoot starter-data-mongodb 进行增删改查 1.什么是MongoDB ? MongoDB 是由C++语言编写的,是一个基于分布式文件 ...

  9. Spring Boot 中使用@Async实现异步调用,加速任务执行!

    欢迎关注方志朋的博客,回复"666"获面试宝典 什么是"异步调用"?"异步调用"对应的是"同步调用",同步调用指程序按照 ...

最新文章

  1. 微信小程序swiper组件宽高自适应方法
  2. HDU 1431 素数回文
  3. Windows socket c++ TCP UDP 简单客户端 vs2013
  4. arp欺骗攻击——获取内网中用户浏览的图片信息
  5. 福建省计算机二级c语言题型,计算机二级C语言题型和评分标准
  6. Oracle数据库php短连接,PHP 连接 Oracle
  7. WSS连接服务器端报错
  8. 【BZOJ4653】区间,离散化+线段树
  9. 去掉chorme浏览器自动补全时input框的背景样式
  10. 2021-2025年中国电子风扇速度控制器行业市场供需与战略研究报告
  11. mssql-sqlserver入门必备知识收集
  12. 暂别ACM,转移阵地
  13. 步步为营-45-一套增删查改
  14. 微软云加速器助edoc2入云腾飞
  15. fri什么意思_卡西欧fri什么意思
  16. Visual Studio 2022自定义(透明)主题和壁纸完整版
  17. 一个简单的Appium测试(Python语言)
  18. Android 10.0 webview版本升级的方法
  19. 记一次 nginx的rewrite和proxy_pass操作
  20. 如何实现uniapp热区链接

热门文章

  1. pyqt5实战之使用画布显示缩略图
  2. python写入csv文件的几种方法
  3. 三安光电圈钱凶猛 两年三轮再融资逾百亿
  4. XAMPP 配置虚拟域名/localhost重定向
  5. XXL-CONF v1.4.1 发布,分布式配置管理平台
  6. centos7快速搭建LAMP
  7. Eclipse SDK构建J2EE开发环境
  8. 设计中最常用的CSS选择器
  9. Concourse:可扩展的开源CI管道工具
  10. 用Docker搭建PHP开发环境