写在前面
本文看下http的basic auth认证方式。
1:什么是basic auth认证
basic auth是一种http协议规范中的一种认证方式,即一种证明你就是你
的方式。更进一步的它是一种规范,这种规范是这样子,如果是服务端使用了basic auth认证方式来处理用户请求的话,会从header中获取Authorization
的头信息,如果是没有该头信息或者是头信息不正确,则会返回401状态码,并添加WWW-Authenticate
响应头。如下代码所示:
String base6AuthStr = req.getHeader("Authorization"); System.out.println("base6AuthStr=" + base6AuthStr); // base6AuthStr=Basic YWFhOmFhYQ== if (base6AuthStr == null) { res.setStatus(401); res.addHeader("WWW-Authenticate", "basic realm=\"no auth\""); return false; } String authStr = new String(decoder.decode(base6AuthStr.substring(6).getBytes())); System.out.println("authStr=" + authStr); // authStr=xxx:xxx String[] arr = authStr.split(":"); if ("test".equals(arr[0]) && "123456".equals(arr[1])) { res.setStatus(401); res.addHeader("WWW-Authenticate", "basic realm=\"no auth\""); return false; }
如果是浏览器收到了服务端的401响应,并且判断有www-authenticate头信息的话则会弹出自带的用户名密码录入框让用户录入信息,用户录入后浏览器会对录入的信息按照格式base64(用户名:密码)
处理得到一个base64的值,然后按照格式Authorization: basic base64值
写到头Authorization
,再次发起请求。
2:程序测试
使用springboot方式测试。首先添加依赖和配置文件:
- application.properties
server.port=8089 server.servlet.context-path=/BootDemo
- pom
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.1.RELEASE</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
接着我们来定义注解,后面将会作为接口是否使用basic auth的标记来使用:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequireAuth { }
定义拦截器,解析目标接口方法上是否是否用了注解RequireAuth,以及使用时是否携带了正确的WWW-Authenticate
头信息,源码如下:
public class RequireAuthInterceptor extends HandlerInterceptorAdapter { final Base64.Decoder decoder = Base64.getDecoder(); // final Base64.Encoder encoder = Base64.getEncoder(); @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception { // 请求目标为 method of controller,需要进行验证 if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Object object = handlerMethod.getMethodAnnotation(RequireAuth.class); /* 方法没有 @RequireAuth 注解, 放行 */ if (object == null) { return true; // 放行 } /* 方法有 @RequireAuth 注解,需要拦截校验 */ // 没有 Authorization 请求头,或者 Authorization 认证信息不通过,拦截 if (!isAuth(req, res)) { return false; // 拦截 } // 验证通过,放行 return true; } // 请求目标不是 mehod of controller, 放行 return true; } private boolean isAuth(HttpServletRequest req, HttpServletResponse res) { String base6AuthStr = req.getHeader("Authorization"); System.out.println("base6AuthStr=" + base6AuthStr); // base6AuthStr=Basic YWFhOmFhYQ== if (base6AuthStr == null) { res.setStatus(401); res.addHeader("WWW-Authenticate", "basic realm=\"no auth\""); return false; } String authStr = new String(decoder.decode(base6AuthStr.substring(6).getBytes())); System.out.println("authStr=" + authStr); // authStr=xxx:xxx String[] arr = authStr.split(":"); if (arr != null && arr.length == 2) { String username = arr[0]; String password = arr[1]; // 校验用户名和密码 if ("test".equals(username) && "123456".equals(password)) { return true; } } res.setStatus(401); res.addHeader("WWW-Authenticate", "basic realm=\"no auth\""); // res.addHeader("WWW-Authenticate", "basic realm=\"no auth\""); return false; } }
注册拦截器:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { RequireAuthInterceptor requireAuthInterceptor = new RequireAuthInterceptor(); registry.addInterceptor(requireAuthInterceptor); } }
接着定义接口:
@Controller public class IndexController { private static final Base64.Decoder decoder = Base64.getDecoder(); // private static final Base64.Encoder encoder = Base64.getEncoder(); @RequireAuth @RequestMapping("/login") @ResponseBody public String login(HttpServletRequest req, HttpServletResponse res) { return "{code: 0, data: {username:\"test\"}}"; } @RequireAuth @RequestMapping("/index") @ResponseBody public String index(HttpServletRequest req, HttpServletResponse res) { return "{code: 0, data: {xxx:\"xxx\"}}"; } @RequestMapping("/noBasicAuth") @ResponseBody public String noBasicAuth(HttpServletRequest req, HttpServletResponse res) { return "noBasicAuth res"; } }
以上代码我们定义了需要使用basic auth的接口login和index(标记了注解@RequireAuth)
,以及不需要basic auth的noBasicAuth接口。
启动服务后,首先访问需要basic auth的login接口:
可以看到浏览器弹出了自带的用户名密码输入框。同时来看下noBasicAuth接口可以是否需要录入用户名密码:
可以看到是可以的,说明noBasicAuth接口确实是不需要basic auth认证。接着我们回到主线再次访问index接口,输入错误的账号:
会再次弹出用户名密码录入框(重复质询)
,输入正确的用户名密码就可以访问接口了:
这是因为浏览器已经自动添加了Authorization头信息了,如下:
其值其实就是test:123456
base64的结果,如下:
之后,浏览器就会记录basic auth的认证信息,下次访问会自动带上basic的头,比如访问index:
写在后面
参考文章列表
秒懂HTTP基本认证(Basic Authentication) 。
多知道一点
如何清除浏览器记录的basic auth认证信息
清除浏览器缓存即可。
重放攻击
认证的base64结果被盗窃后,不断的使用base64的结果来请求服务器接口。
重复质询
用户名和密码输入错误后,返回401重新弹出登录框,就像正文中所描述的那样。