前言
今天跟小伙伴们分享一个实战内容,使用Spring Boot+Shiro实现一个简单的Http认证。
场景是这样的,我们平时的工作中可能会对外提供一些接口,如果这些接口不做一些安全认证,什么人都可以访问,安全性就太低了,所以我们的目的就是增加一个接口的认证机制,防止别人通过接口攻击服务器。
至于Shiro是什么,Http的Basic认证是什么,王子就简单介绍一下,详细内容请自行了解。
Shiro是一个Java的安全框架,可以简单实现登录、鉴权等等的功能。
Basic认证是一种较为简单的HTTP认证方式,客户端通过明文(Base64编码格式)传输用户名和密码到服务端进行认证,通常需要配合HTTPS来保证信息传输的安全。
实践部分
首先说明一下测试环境。
王子已经有了一套集成好Shiro的Spring Boot框架,这套框架详细代码就不做展示了,我们只来看一下测试用例。
要测试的接口代码如下:
/** * @author liumeng */ @RestController @RequestMapping("/test") @CrossOrigin public class TestAppController extends BaseController { /** * 数据汇总 */ @GetMapping("/list") public AjaxResult test() { return success("测试接口!"); } }
使用Shiro,一定会有Shiro的拦截器配置,这部分代码如下:
/** * Shiro过滤器配置 */ @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // Shiro的核心安全接口,这个属性是必须的 shiroFilterFactoryBean.setSecurityManager(securityManager); // 身份认证失败,则跳转到登录页面的配置 shiroFilterFactoryBean.setLoginUrl(loginUrl); // 权限认证失败,则跳转到指定页面 shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl); // Shiro连接约束配置,即过滤链的定义 LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // 对静态资源设置匿名访问 filterChainDefinitionMap.put("/favicon.ico**", "anon"); filterChainDefinitionMap.put("/lr.png**", "anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/docs/**", "anon"); filterChainDefinitionMap.put("/fonts/**", "anon"); filterChainDefinitionMap.put("/img/**", "anon"); filterChainDefinitionMap.put("/ajax/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/lr/**", "anon"); filterChainDefinitionMap.put("/captcha/captchaImage**", "anon"); // 退出 logout地址,shiro去清除session filterChainDefinitionMap.put("/logout", "logout"); // 不需要拦截的访问 filterChainDefinitionMap.put("/login", "anon,captchaValidate"); filterChainDefinitionMap.put("/ssoLogin", "anon");
// 开启Http的Basic认证 filterChainDefinitionMap.put("/test/**", "authcBasic"); // 注册相关 filterChainDefinitionMap.put("/register", "anon,captchaValidate"); Map<String, Filter> filters = new LinkedHashMap<String, Filter>(); filters.put("onlineSession", onlineSessionFilter()); filters.put("syncOnlineSession", syncOnlineSessionFilter()); filters.put("captchaValidate", captchaValidateFilter()); filters.put("kickout", kickoutSessionFilter()); // 注销成功,则跳转到指定页面 filters.put("logout", logoutFilter()); shiroFilterFactoryBean.setFilters(filters); // 所有请求需要认证authcBasic filterChainDefinitionMap.put("/**", "user,kickout,onlineSession,syncOnlineSession"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; }
这里我们要关注的是代码中的
filterChainDefinitionMap.put("/test/**", "authcBasic");
这部分代码,它指定了我们的测试接口启动了Http的Basic认证,这就是我们的第一步。
做到这里我们可以尝试的去用浏览器访问一下接口,会发现如下情况:
这就代表Basic认证已经成功开启了,这个时候我们输入系统的用户名和密码,你以为它就能成功访问了吗?
答案是否定的,我们只是开启了认证,但并没有实现认证的逻辑。
王子通过阅读部分Shiro源码,发现每次发送请求后,都会调用ModularRealmAuthenticator这个类的doAuthenticate方法,源码如下:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }
可以看出,这个方法主要就是对Realm进行了管理,因为我们的系统本身已经有两个Ream了,针对的是不同情况的权限验证,所以为了使用起来不冲突,我们可以继承这个类来实现我们自己的逻辑,在配置类中增加如下内容即可:
@Bean public ModularRealmAuthenticator modularRealmAuthenticator(){ //用自己重新的覆盖 UserModularRealmAuthericator modularRealmAuthericator = new UserModularRealmAuthericator(); modularRealmAuthericator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); return modularRealmAuthericator; }
然后在我们自己的UserModularRealmAuthericator类中重写doAuthenticate方法就可以了,这里面的具体实现逻辑就要看你们自己的使用场景了。
我们可以自己新创建一个Realm来单独校验Basic认证的情况,或者共用之前的Realm,这部分就*发挥了。
大概内容如下:
public class UserModularRealmAuthericator extends ModularRealmAuthenticator { private static final Logger logger = LoggerFactory.getLogger(UserModularRealmAuthericator.class); @Override protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); //强制转换返回的token UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;//所有Realm Collection<Realm> realms = getRealms(); //最终选择的Realm Collection<Realm> typeRealms = new ArrayList<>(); for(Realm realm:realms){ if(...){ //这部分是自己的逻辑判断,过滤出想要使用的Realm typeRealms.add(realm); } } //判断是单Realm 还是多Realm if(typeRealms.size()==1){ return doSingleRealmAuthentication(typeRealms.iterator().next(),usernamePasswordToken); }else{ return doMultiRealmAuthentication(typeRealms,usernamePasswordToken); } } }
Realm的具体实现代码这里就不做演示了,无非就是判断用户名密码是否能通过校验的逻辑。如果不清楚,可以自行了解Realm的实现方式。
Realm校验实现后,Basic认证就已经实现了。
测试部分
接下来我们再次使用浏览器对接口进行测试,输入用户名和密码,就会发现接口成功响应了。
我们来抓取一下请求情况
可以发现,Request Header中有了Basic认证的信息Authorization: Basic dGVzdDoxMjM0NTY=
这部分内容是这样的,Basic为一个固定的写法,dGVzdDoxMjM0NTY=这部分内容是userName:Password组合后的Base64编码,所以我们只要给第三方提供这个编码,他们就可以通过编码访问我们的接口了。
使用PostMan测试一下
可以发现接口是可以成功访问的。
总结
到这里本篇文章就结束了,王子向大家仔细的介绍了如何使用Shiro实现一个Http请求的Basic认证,是不是很简单呢。
给大家留下一个思考题,你认为这种Basic认证能够保证接口的安全吗?
欢迎大家留言讨论。
往期文章推荐: