问题场景:
接口需要经过Basic Auth认证之后才能继续访问
假如项目需要把一些接口加上安全认证访问限制,比如这里使用比较简单的Basic Auth认证,那么应该如何使用tomcat的自带的认证机制来完成访问限制呢。
解决方案
如果不借助tomcat的认证机制来完成接口访问限制,最先想到的就是自己写一个Filter类,拦截需要认证的接口,如果用户请求头中传入的Authorization字段解析后用户名和密码不对就给用户响应一个401状态码,这种自定义Filter参考之前的文章。
针对SpringBoot Actuator未授权访问端点问题
下面演示如何用tomcat提供的类完成认证,先看下代码整体思路
package com.promote.demo.config;
import lombok.SneakyThrows;
import org.apache.catalina.Context;
import org.apache.catalina.authenticator.BasicAuthenticator;
import org.apache.catalina.connector.Request;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.realm.RealmBase;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.context.annotation.Configuration;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class WebSecurityConfig implements TomcatContextCustomizer {
@Value("${basic.auth.username}")
private String username;
@Value("${basic.auth.password}")
private String password;
@SneakyThrows
@Override
public void customize(Context context) {
CustomAuthenticator authenticator = new CustomAuthenticator();
authenticator.setCharset("UTF-8");
//定义数据源realm这里提供两种方式
//1.方式一:使用一个内存数据源来存放用户名和密码,也有很多其他的realm可以自行研究
//MemoryRealm realm = new MemoryRealm();
//把用户名和密码放在类路径下
//realm.setPathname("classpath:users.xml");
//把用户名和密码放在某个文件路径下
//realm.setPathname("D:\\data\\users.xml");
//2.方式二:自定义一个realm,可以更灵活,比如这里的用户名和密码可以自己配置在配置文件中
CustomRealm realm = new CustomRealm();
realm.addUser(username, password);
context.setRealm(realm);
//当请求过来的时候会先进入tomcat的pipeline中,每个pipeline有多个value组件来处理请求,类似于Filter链;
//这里我们定义的CustomAuthenticator也是属于value组件,当请求在管道中的所有组中走一遍之后才会到达Filter
context.getPipeline().addValve(authenticator);
}
/**
* BasicAuthenticator里面已经做了相关的身份验证逻辑
*/
public static class CustomAuthenticator extends BasicAuthenticator {
/**
* 判断哪些路径需要权限校验,可以根据实际项目进行判定
*
* @param request The request currently being processed
* @return 是否需要权限校验
*/
@Override
protected boolean isContinuationRequired(Request request) {
String path = request.getRequestURI();
//这里假设/api路径开头的url都需要进行权限校验
return path.startsWith("/api");
}
}
/**
* 可以自定义Realm需要继承抽象类RealmBase,并实现getPassword和getPrincipal方法
*/
public static class CustomRealm extends RealmBase {
private final Map
/**
* 添加用户
*
* @param username 用户名
* @param password 密码
*/
public void addUser(String username, String password) {
principals.put(username, new GenericPrincipal(username, password, new ArrayList<>()));
}
@Override
protected String getPassword(String username) {
GenericPrincipal principal = principals.get(username);
if (principal != null) {
return principal.getPassword();
} else {
return null;
}
}
@Override
protected Principal getPrincipal(String username) {
return principals.get(username);
}
}
}
步骤一: 首先我们需要写一个配置类实现TomcatContextCustomizer的接口,这个接口是springboot启动阶段提供给用户操作tomcat容器的接口,可以灵活配置一些业务。 步骤二: 我们需要借助tomcat中的BasicAuthenticator类来完成Basic Auth认证,这里面有比较成熟的Basic Auth认证逻辑,直接拿来使用。需要写一个类继承BasicAuthenticator并重写isContinuationRequired方法,在这个方法里面灵活判断哪些接口需要权限认证,这里我们定义/api开头接口都需要认证,然后把这个BasicAuthenticator类加到tomcat容器的pipeline中。tomcat还提供了其他的认证方式,如表单认证,这个是基于session的,感兴趣的可以自行测试。 步骤三: 我们需要定一个数据源来存放用户名和密码,这个数据源在tomcat中叫Realm,和Shrio中的Realm有些类似,tomcat默认提供了一些Realm,有内存Realm,数据库Realm等,看下Realm的关系图。 如果使用MemoryRealm,需要配置一个xml文件,文件内容如下,其实就是和tomcat安装包confg目录下的tomcat-users.xml文件内容是一样的,需要指定这个文件的位置,可以放在类路径下,也可以放在磁盘路径下。
由于是使用spring的配置文件来管理用户名和密码,不想使用MemoryRealm,这里自定义了一个CustomRealm来存放用户名和密码,比较灵活。
其他详细解释参考代码注释
运行效果图
未认证界面效果 取消认证效果
结语
由于认证机制是在tomcat的pipeline中完成的,还没有走到Filter,只有认证通过请求才会继续走到Filter然后再走到springmvc的interceptor,所以一定程度上性能会更好一点
tomcat的pipeline是什么参考这篇文章
文章链接
发表评论