HTTP 接口鉴权情景说明
鉴权,有很多方案,如:SpringSecurity、Shiro、拦截器、过滤器等等。如果只是对一些URL进行认证鉴权的话,我们完全没必要引入SpringSecurity或Shiro等框架,使用拦截器或过滤器就足以实现需求,也可以独自使用AOP结合注解的形式实现。在某种情况下,针对指定的方法做权限验证。尤其是一个请求地址下,例如 api/public/** 下面有多个请求方法,这个请求下有部分需要公开的接口,部分需要增加权限的接口,这个时候就不能使用通过拦截url的形式了。可以考虑AOP+注解的形式去实现
我们需要提供的 HTTP RESTful 服务, 这个服务会提供一些比较敏感的信息, 因此对于某些接口的调用会进行调用方权限的校验, 而某些不太敏感的接口则不设置权限, 或所需要的权限比较低(例如某些监控接口, 服务状态接口等).
实现这样的需求的方法有很多, 例如我们可以在每个 HTTP 接口方法中对服务请求的调用方进行权限的检查, 当调用方权限不符时, 方法返回错误. 当然这样做并无不可, 不过如果我们的 api 接口很多, 每个接口都进行这样的判断, 无疑有很多冗余的代码, 并且很有可能有某个粗心的家伙忘记了对调用者的权限进行验证, 这样就会造成潜在的 bug.
接下来我们使用以下几种方式做API鉴权
查了相关资料,推荐使用拦截器和 Spring AOP 的形式,关于拦截器和过滤器的区别参考
https://blog.csdn.net/qq_36411874/article/details/53996873
https://blog.csdn.net/qq_34871626/article/details/79185829
(1)过滤器(Filter):当你传入的request、response 需要提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者struts的action进行业务逻辑,比如过滤掉非法url(不是login.do的地址请求,如果用户没有登陆都过滤掉),或者在传入servlet或者 struts的action前统一设置字符集,或者去除掉一些非法字符.。,你只希望选择符合你要求的某一些东西。定义这些要求的工具,就是过滤器。
(2)拦截器(Interceptor):在一个流程正在进行的时候,你希望干预它的进展,甚至终止它进行,这是拦截器做的事情。例如日志、事务,以及方法耗时监控。
(3)Spring AOP : 只能拦截Spring管理Bean的访问(业务层Service)
(一)使用过滤器实现
1.第一步引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2.新建SignAutheFilter类
package com.example.jsr303.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 使用过滤器实现http接口鉴权
*
* @WebFilter注解指定要被过滤的URL 一个URL会被多个过滤器过滤时, 还可以使用@Order(x)来指定过滤request的先后顺序,x数字越小越先过滤
*/
@WebFilter(urlPatterns = {"/authen/test1", "/authen/test2", "/authen/test3"})
public class SignAutheFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(SignAutheFilter.class);
@Value("${secret}")
private String secret;
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
String authorization = request.getHeader("Authorization");
logger.info("getted Authorization is ---> " + authorization);
if (secret.equals(authorization)) {
chain.doFilter(request, response);
return;
}
response.sendError(403, "Forbidden");
} catch (Exception e) {
logger.error("AuthenticationFilter -> " + e.getMessage(), e);
response.sendError(403, "Forbidden");
}
}
@Override
public void destroy() {
}
}
3.在application.properties配置secret
4.请求结果
接下来输入一个错误的secret
image.png
secret输入正确后,发现权限访问成功
image.png
(二)使用拦截器实现
接下来需要将第一个过滤器案例中 SignAutheFilter 类删除或者注释后再去使用拦截器实现鉴权:
//@WebFilter(urlPatterns = {"/authen/test1", "/authen/test2", "/authen/test3"})
public class SignAutheFilter implements Filter {
1.引入 spring-boot-starter-web ,前面已经引入过后,就不再叙述
2.新建ApiInterceptor拦截器 继承 HandlerInterceptorAdapter 或者实现 HandlerInterceptor
package com.example.jsr303.interceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Component
public class ApiInterceptor extends HandlerInterceptorAdapter {
@Value("${secret}")
private String secret;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
String authorization = request.getHeader("Authorization");
//进行校验
if(secret.equals(authorization)){
return true;
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.append("{\"message\":\"拒绝访问\"}");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (writer != null) {
writer.close();
}
}
return false;
}
}
3.将拦截器注册到InterceptorRegistry 中 新建 ServletContextConfiguration类实现WebMvcConfigurer
package com.example.jsr303.config;
import com.example.jsr303.interceptor.ApiInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
@Configuration
public class ServletContextConfiguration implements WebMvcConfigurer {
@Autowired
ApiInterceptor apiInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiInterceptor);
}
}
注意:拦截器必须要添加注册,否则不能正常拦截请求
如果拦截器注册了。但是未配置拦截url一样的无法正常拦截的例如:
image.png
修改配置拦截的url路径后
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration apiInterceptorRegistration = registry.addInterceptor(apiInterceptor);
// 配置拦截的路径
apiInterceptorRegistration.addPathPatterns("/authen/**");
// 配置不拦截的路径
apiInterceptorRegistration.excludePathPatterns("/api/public/**");
}
输入一个错误中的secret请求如下:
image.png
当然secret正确的情况下请求如下:
image.png
3.使用Spring AOP 或加注解的形式做鉴权实现
1.引入AOP相关依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.6.8</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.8</version>
</dependency>
2.具体AOP的应用实现可以参考https://blog.csdn.net/limengliang4007/article/details/78660834
到此实现鉴权的方式都介绍完了,除了以上通过secret的形式,我们还可以在代码中设置固定IP的形式去做更细粒的鉴权控制。
获取IP地址的博文可以参考https://www.cnblogs.com/xiaoxing/p/6565573.html
最后讲一下增加ip鉴权验证
(1)新建IpUtil类
package com.example.jsr303.common;
import javax.servlet.http.HttpServletRequest;
public class IpUtil {
/**
* 获取用户真实IP地址,不使用request.getRemoteAddr()的原因是有可能用户使用了代理软件方式避免真实IP地址,
* 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值
*
* @return ip
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
System.out.println("x-forwarded-for ip: " + ip);
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if (ip.indexOf(",") != -1) {
ip = ip.split(",")[0];
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
System.out.println("Proxy-Client-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
System.out.println("WL-Proxy-Client-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
System.out.println("HTTP_CLIENT_IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
System.out.println("HTTP_X_FORWARDED_FOR ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
System.out.println("X-Real-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
System.out.println("getRemoteAddr ip: " + ip);
}
System.out.println("获取客户端ip: " + ip);
return ip;
}
}
(2)配置application.properties 并且代码中使用
image.png
@Value("${secret}")
private String[] permittedIps;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
String authorization = request.getHeader("Authorization");
String ipAddr = IpUtil.getIpAddr(request);
boolean ipFlag = false;
for (String ip : permittedIps) {
if(ipAddr.equals(ip)){
ipFlag = true;
break;
}
}
//ip 和 secret 双重验证
if(ipFlag && secret.equals(authorization)){
return true;
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.append("{\"message\":\"拒绝访问\"}");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (writer != null) {
writer.close();
}
}
return false;
}
接下来请求会发现如果没有指定ip,是无权限访问的。
涉及到签名相关的需要自行百度。
总结:
1.鉴权实现方式可以通过拦截器,过滤器,和spring AOP + 注解的形式
2.推荐使用拦截器 和spring AOP + 注解的形式
3.spring AOP + 注解的形式 可以在特殊的情况下,只针对部分接口做鉴权验证。
网友评论