无状态Spring安全性第2部分:无状态身份验证

Spring Stateless Security系列的第二部分是关于以无状态方式探索身份验证的方法。 如果您错过了CSRF的第一部分,可以在这里找到。

因此,在谈论身份验证时,其全部内容就是让客户端以可验证的方式向服务器标识自己。 通常,这始于服务器向客户端提供挑战,例如要求填写用户名/密码的请求。 今天,我想着重介绍在通过此类初始(手动)挑战后会发生什么情况,以及如何处理其他HTTP请求的自动重新身份验证。

常用方法

基于会话Cookie

我们可能最了解的最常见方法是使用服务器生成的JSESSIONID cookie形式的秘密令牌(会话密钥)。 这些天的初始设置几乎没有用,也许会让您忘记,您有一个选择要放在这里。 即使没有进一步使用此“会话密钥”来存储“会话中”的任何其他状态,该密钥本身实际上也是状态 。 即,如果没有这些密钥的共享和持久存储,则成功的身份验证将无法在服务器重新启动或请求负载平衡到另一台服务器后继续存在。

OAuth2 / API密钥

每当谈论REST API和安全性时; 提到了OAuth2和其他类型的API密钥。 基本上,它们涉及在HTTP授权标头中发送自定义令牌/密钥。 如果使用得当,两种方法都可以避免客户端使用标头来处理Cookie。 这解决了CSRF漏洞和其他Cookie相关问题。 但是,他们无法解决的一件事是服务器需要检查显示的身份验证密钥,几乎需要一些持久且可维护的共享存储来将密钥链接到用户/授权。

无状态方法

1. HTTP基础认证

处理认证的最古老,最粗糙的方式。 只需让用户随每个请求发送其用户名/密码。 这听起来似乎很可怕,但是考虑到上述任何方法也都通过网​​络发送秘密密钥,这实际上并不是那么安全。 主要是用户体验和灵活性,这使得其他方法成为更好的选择。

2.服务器签名的令牌

以无状态方式处理请求中的状态的一个巧妙小技巧是让服务器对其“签名”。 然后可以在每个请求之间在客户端/服务器之间来回传输该请求,并确保它不会受到限制。 这样,任何用户标识数据都可以以纯文本形式共享,并为其添加特殊的签名哈希。 考虑到已签名,服务器可以简单地验证签名哈希是否仍与接收到的内容匹配,而无需保持任何服务器端状态。

可以用于此目的的通用标准是JSON Web令牌 (JWT),该标准仍在起草中。 对于本博客文章,我想摆脱困境,跳过完全的合规性以及使用附带的库的尖叫声。 从中挑选我们真正需要的东西。 (省略了标头/变量哈希算法和url-safe base64编码)

实作

如前所述,我们将使用Spring Security和Spring Boot将自己的实现整合在一起。 没有任何库或精美的API会混淆令牌级别上真正发生的事情。 令牌在伪代码中看起来像这样:

content = toJSON(user_details)
token = BASE64(content) + "." + BASE64(HMAC(content))

令牌中的用作分隔符,因此每个部分都可以分别标识和解码,因为点字符不是任何base64编码字符串的一部分。 HMAC代表基于哈希的消息身份验证代码,它基本上是使用预定义密钥从任何数据中生成的哈希。

在实际的Java中,令牌的生成与伪代码非常相似:

创建令牌

public String createTokenForUser(User user) {byte[] userBytes = toJSON(user);byte[] hash = createHmac(userBytes);final StringBuilder sb = new StringBuilder(170);sb.append(toBase64(userBytes));sb.append(SEPARATOR);sb.append(toBase64(hash));return sb.toString();
}

JSON中使用的相关User属性是id,username,expires和role ,但可以是您真正想要的任何东西。 我标记了杰克逊JSON序列化期间将忽略的User对象的“ password”属性,因此它不会成为令牌的一部分:

忽略密码

@JsonIgnore
public String getPassword() {return password;
}

对于现实世界的场景,您可能只想为此使用专用对象。

通过一些输入验证来防止/捕获由于对令牌进行调整而导致的解析错误,令牌的解码会稍微复杂一些:

解码令牌

public User parseUserFromToken(String token) {final String[] parts = token.split(SEPARATOR_SPLITTER);if (parts.length == 2 && parts[0].length() > 0 && parts[1].length() > 0) {try {final byte[] userBytes = fromBase64(parts[0]);final byte[] hash = fromBase64(parts[1]);boolean validHash = Arrays.equals(createHmac(userBytes), hash);if (validHash) {final User user = fromJSON(userBytes);if (new Date().getTime() < user.getExpires()) {return user;}}} catch (IllegalArgumentException e) {//log tampering attempt here}}return null;
}

它本质上验证提供的哈希值是否与内容的新计算哈希值相同。 因为createHmac方法在内部使用未公开的秘密密钥来计算哈希,所以没有客户端能够调整内容并提供与服务器生成的哈希相同的哈希。 仅在通过此测试后,提供的数据才会被解释为表示User对象的JSON。

放大Hmac部分,让我们看一下所涉及的Java。 首先,必须使用一个私钥对其进行初始化,这是TokenHandler的构造函数的一部分:

HMAC初始化

...
private static final String HMAC_ALGO = "HmacSHA256";private final Mac hmac;public TokenHandler(byte[] secretKey) {try {hmac = Mac.getInstance(HMAC_ALGO);hmac.init(new SecretKeySpec(secretKey, HMAC_ALGO));} catch (NoSuchAlgorithmException | InvalidKeyException e) {throw new IllegalStateException("failed to initialize HMAC: " + e.getMessage(), e);}
}
...

初始化后,可以使用一个方法调用(重新)使用它! (doFinal的JavaDoc读取“处理给定的字节数组并完成MAC操作。对该方法的调用会将这个Mac对象重置为先前通过调用init(Key)或init(Key,AlgorithmParameterSpec进行初始化)时所处的状态。 …”)

createHmac

// synchronized to guard internal hmac object
private synchronized byte[] createHmac(byte[] content) {return hmac.doFinal(content);
}

我在这里使用了一些粗略的同步,以防止在Spring Singleton Service中使用时发生冲突。 实际的方法非常快(〜0.01ms),因此除非您每台服务器每秒要发送10k +请求,否则它不会造成任何问题。

说到服务,让我们一路攀升到完全可运行的基于令牌的身份验证服务:

令牌认证服务

@Service
public class TokenAuthenticationService {private static final String AUTH_HEADER_NAME = "X-AUTH-TOKEN";private static final long TEN_DAYS = 1000 * 60 * 60 * 24 * 10;private final TokenHandler tokenHandler;@Autowiredpublic TokenAuthenticationService(@Value("${token.secret}") String secret) {tokenHandler = new TokenHandler(DatatypeConverter.parseBase64Binary(secret));}public void addAuthentication(HttpServletResponse response, UserAuthentication authentication) {final User user = authentication.getDetails();user.setExpires(System.currentTimeMillis() + TEN_DAYS);response.addHeader(AUTH_HEADER_NAME, tokenHandler.createTokenForUser(user));}public Authentication getAuthentication(HttpServletRequest request) {final String token = request.getHeader(AUTH_HEADER_NAME);if (token != null) {final User user = tokenHandler.parseUserFromToken(token);if (user != null) {return new UserAuthentication(user);}}return null;}
}

很简单,初始化一个私有TokenHandler来完成繁重的工作。 它提供了添加和读取自定义HTTP令牌标头的方法。 如您所见,它不使用任何(数据库驱动的)UserDetailsS​​ervice查找用户详细信息。 通过令牌提供了让Spring Security处理进一步的授权检查所需的所有详细信息。
最后,我们现在可以将所有这些插件插入到Spring Security中,在Security配置中添加两个自定义过滤器:

StatelessAuthenticationSecurityConfig内部的安全配置

...
@Override
protected void configure(HttpSecurity http) throws Exception {http...// custom JSON based authentication by POST of // {"username":"<name>","password":"<password>"} // which sets the token header upon authentication.addFilterBefore(new StatelessLoginFilter("/api/login", ...), UsernamePasswordAuthenticationFilter.class)// custom Token based authentication based on // the header previously given to the client.addFilterBefore(new StatelessAuthenticationFilter(...), UsernamePasswordAuthenticationFilter.class);
}
...

StatelessLoginFilter在成功认证后添加令牌:

StatelessLoginFilter

...
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,FilterChain chain, Authentication authentication) throws IOException, ServletException {// Lookup the complete User object from the database and create an Authentication for itfinal User authenticatedUser = userDetailsService.loadUserByUsername(authentication.getName());final UserAuthentication userAuthentication = new UserAuthentication(authenticatedUser);// Add the custom token as HTTP header to the responsetokenAuthenticationService.addAuthentication(response, userAuthentication);// Add the authentication to the Security contextSecurityContextHolder.getContext().setAuthentication(userAuthentication);
}
...

StatelessAuthenticationFilter仅根据标头设置身份验证:

StatelessAuthenticationFilter

...
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,	ServletException {SecurityContextHolder.getContext().setAuthentication(tokenAuthenticationService.getAuthentication((HttpServletRequest) req));chain.doFilter(req, res); // always continue
}
...

请注意,与大多数与Spring Security相关的过滤器不同,无论身份验证成功如何,我都选择继续沿过滤器链向下移动。 我想支持触发Spring的AnonymousAuthenticationFilter以支持匿名身份验证。 这里最大的区别是过滤器未配置为映射到任何专门用于身份验证的URL,因此不提供标头并不是真正的问题。

客户端实施

客户端实现同样非常简单。 再次,我将其保持为最低限度,以防止在AngularJS详细信息中丢失身份验证位。 如果您正在寻找一个更完整地与路由集成的AngularJS JWT示例,则应在此处查看 。 我从中借用了一些拦截器逻辑。
登录只需存储令牌(在localStorage中 ):

登录

$scope.login = function () {var credentials = { username: $scope.username, password: $scope.password };$http.post('/api/login', credentials).success(function (result, status, headers) {$scope.authenticated = true;TokenStorage.store(headers('X-AUTH-TOKEN'));});  
};

注销甚至更简单(无需调用服务器):

登出

$scope.logout = function () {// Just clear the local storageTokenStorage.clear();	$scope.authenticated = false;
};

要检查用户是否“已经登录”,ng-init =“ init()”可以很好地工作:

在里面

$scope.init = function () {$http.get('/api/users/current').success(function (user) {if(user.username !== 'anonymousUser'){$scope.authenticated = true;$scope.username = user.username;}});
};

我选择使用匿名可访问的端点来防止触发401/403。 您也可以解码令牌本身并检查到期时间,并相信本地客户端时间足够准确。

最后,为了使添加标头的过程自动化,就像上一个博客条目中那样,一个简单的拦截器很好地做到了:

令牌验证拦截器

factory('TokenAuthInterceptor', function($q, TokenStorage) {return {request: function(config) {var authToken = TokenStorage.retrieve();if (authToken) {config.headers['X-AUTH-TOKEN'] = authToken;}return config;},responseError: function(error) {if (error.status === 401 || error.status === 403) {TokenStorage.clear();}return $q.reject(error);}};
}).config(function($httpProvider) {$httpProvider.interceptors.push('TokenAuthInterceptor');
});

假设客户端不会允许调用需要更高特权的区域,它还会照顾到在收到HTTP 401或403之后自动清除令牌的情况。

令牌存储

TokenStorage只是对localStorage的包装服务,我不会打扰您。 将令牌放入localStorage可以防止脚本像保存cookie一样在保存脚本的脚本源之外读取脚本。 但是,由于令牌不是实际的Cookie,因此无法指示任何浏览器将其自动添加到请求中。 这是至关重要的,因为它可以完全防止任何形式的CSRF攻击。 因此,您不必实施我以前的博客中提到的任何(无状态)CSRF保护。

  • 您可以在github上找到一个完整的工作示例,其中包含一些不错的功能。

确保已安装gradle 2.0,并使用“ gradle build”和“ gradle run”简单地运行它。 如果要像Eclipse一样在IDE中使用它,请使用“ gradle eclipse”,只需从IDE内导入并运行它即可(无需服务器)。

翻译自: https://www.javacodegeeks.com/2014/10/stateless-spring-security-part-2-stateless-authentication.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/361948.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TypeScript 交叉类型(intersection type)

在TS中和联合类型(union type)对应的还有交叉类型(intersection type)。 交叉类型的出现主要为了组合多个对象类型(object type)&#xff0c;因为相对于interface&#xff0c;object type没法继承&#xff0c;那么就可以通过union type来实现混合的目的&#xff0c;从而实现继承…

【转】JAVA中的转义字符

JAVA中转义字符&#xff1a; 1.八进制转义序列&#xff1a;\ 1到3位5数字&#xff1b;范围\000~\377 \0&#xff1a;空字符 2.Unicode转义字符&#xff1a;\u 四个十六进制数字&#xff1b;0~65535 \u0000&#xff1a;空字符 3.特殊字符&#xff1a;就3个 \"&#xff1a…

八、VueJs 填坑日记之参数传递及内容页面的开发

我们在上一篇博文中&#xff0c;渲染出来了一个列表&#xff0c;并在列表中使用了router-link标签&#xff0c;标签内的&#xff1a;to就是链接地址&#xff0c;昨天咱们是<router-link :to"/content/ i.id">这样写的&#xff0c;今天我们来完成内容页面的渲染…

为Kindeditor控件添加图片自动上传功能

Kindeditor是一款功能强大的开源在线HTML编辑器&#xff0c;支持所见即所得的编辑效果。它使用JavaScript编写&#xff0c;可以无缝地与多个不同的语言环境进行集成&#xff0c;如.NET、PHP、ASP、Java等。官方网站可以查看这里&#xff1a;http://kindeditor.net/index.php Ki…

TypeScript类型推论(Type Inference)

要完全理解类型推论需要完整理解类型上下文&#xff0c;并且理解TS对于是否可以使用类型推论是基于静态分析完成的。 上下文类型应用在许多地方。常见的例子包括函数调用的参数&#xff0c;赋值的右手端位置&#xff0c;类型断言&#xff0c;对象和数组的成员&#xff0c;和返回…

4个万无一失的技巧让您开始使用JBoss BRMS 6.0.3

上周&#xff0c;红帽发布了标记为6.0.3的JBoss BRMS的下一版本&#xff0c;已订阅的用户可以在其客户门户中使用。 如果您对该版本的新增功能感到好奇&#xff0c;请在客户门户网站上在线查看版本说明和其余文档 。 我们正在寻找一些简单的方法来开始使用此新版本&#xff0…

统一命名规则

1. #define 保护 所有头文件都应该使用 #define 防止头文件被多重包含, 命名格式当是:<PROJECT>_<PATH>_<FILE>_H_ 项目 SkinTK中的头文件 SkinTK/SkinTK/targetver.h 可按如下方式保护: #ifndef SKINTK_SKINTK_TARGETVER_H_ #define SKINTK_SKINTK_TARGETVE…

中国移动MM7 API用户手册(七)

4.4 VASP接收状态报告&#xff08;上行业务&#xff09;当VASP在发送MM7SubmitReq给MMSC时设置需要发送状态报告的请求为true时&#xff0c;MMSC在收到MM7SubmitReq后&#xff0c;会发送状态报告给VASP&#xff0c;此时VASP可以进行接收。接收方式和接收传送消息一样&#xff…

如何仅通过CSS实现多行文本超长自动省略号

在CSS中&#xff0c;我们可以通过下面的样式实现DIV元素中文本超长后自动截断并以省略号结尾&#xff1a; overflow: hidden;word-break: normal;text-overflow: ellipsis; text-overflow: ellipsis是实现文本截断后以省略号结尾的关键样式&#xff0c;但问题是如果添加该样式则…

带有Angular JS的Java EE 7 – CRUD,REST,验证–第2部分

这是Angular JS承诺的Java EE 7的后续版本–第1部分 。 花了比我预期更长的时间&#xff08;找到时间来准备代码和博客文章&#xff09;&#xff0c;但是终于到了&#xff01; 应用程序 第1部分中的原始应用程序只是带有分页的简单列表&#xff0c;以及提供列表数据的REST服务…

Chrome不显示OPTIONS请求的解决方法2021版chrome90

在chrome90上之前展示跨域请求预检请求的方法失效了&#xff1a; 在chrome地址栏总输入 chrome://flags/#out-of-blink-cors 将其设置为Disabled后重启浏览器 在chrome://flags找不到选项out-of-blink-cors。取而代之的是chrome将预检请求放到了控制台网络面板的 OTHER 面板中。…

安装CentOs 5.5后无法显中文(中文乱码)

症状&#xff1a;在使用CentOS 系统时&#xff0c;安装的时候可能你会遇到英文的CentOS系统&#xff0c;在这中情况下安装CentOS系统时是默认安装&#xff08;即英文&#xff09;。安装完毕后&#xff0c;上网出现的却是中文乱码。 解决方法&#xff1a; 到CentOs资源网站上去找…

粗读《构建之法》后的思考和收获

利用出差的空挡&#xff0c;快速阅读了一下邹欣老师的《构建之法》一书。对我校软件工程的教学改革确实有很多值得参考的地方&#xff0c;强调实践环节和社会实际工作流程的结合&#xff0c;而不是为了实验而实验。 在阅读过程也有一些问题。 问题1&#xff1a;MSF中强调人员的…

SVG实现波浪效果

SVG实现波浪效果 svg path&#xff1a;C 贝塞尔曲线绘制波浪形状 A 绘制圆弧形 svg animate&#xff1a;制作波浪动画&#xff0c;为了波浪动画效果自然&#xff0c;设置values关键点       attributeName&#xff1a;变化属性名 dur&#xff1a;动画时间 repeatCount&a…

允许同站跨域Nginx配置方案

基于目前前后端分离的趋势和微前端解决方案&#xff0c;并且很多web服务部署在Nginx服务器上&#xff0c;那么因为前后端分离导致的跨域问题需要迫切得到解决。因为是否允许跨域的因素有协议、域名、端口&#xff0c;只要有一个不一致就算跨域。大部分需求要求一个一级域名下所…

使用WildFly 8在Java EE7中自举Apache Camel

从Camel版本2.10开始&#xff0c;支持CDI&#xff08;JSR-299&#xff09;和DI&#xff08;JSR-330&#xff09;。 这为在Java EE容器中以及独立Java SE或CDI容器中开发和部署Apache Camel项目提供了新的机会。 是时候尝试一下并熟悉它了。 骆驼到底是什么&#xff1f; 骆驼是…

Larbin源代码分析[6]LARBIN中线程处理类

一 线程类 larbin下的线程操作类&#xff0c;主要在mypthread.h中定义&#xff0c;实质上是利用宏定义&#xff0c;封装了pthread.h中的系统调用。 一个进程可以有多个线程&#xff0c;每个线程都有自己的处理流程。 二 具体实现 typedef void* (*StartFun) (void *); void sta…

好东西要分享

目录 矢量图标库$\text{pic}$图论神器$\text{bzoj}$离线题库打字速度表情包PPP矢量图标库 个人jio的这个阿里巴巴矢量图标库蛮不错的 上面这张就是去上面的网站找的。 $\text{pic}$ 来wallpaper abyss找点好康的图片吧。 图论神器 画图 $\text{bzoj}$离线题库 $\text{bzoj}$离线…

Vue2.0 全家桶开发的网页应用(参照吾记APP)

github链接 借鉴吾记APP&#xff0c;使用 vue2.0 vue-router vuex 为主要技术栈&#xff0c;elementui做为ui框架&#xff0c;多模块 spa 模式&#xff0c;webpack2.0 负责模块打包&#xff0c;gulp 负责处理静态资源打包、压缩&#xff0c;欢迎打赏star&#xff01;&#xff…

【antd】输入控件的思想

antd对于form中输入控件的抽象十分简单&#xff0c;只要能接收value和onChange属性的组件都可以成为Form.Item的子组件&#xff0c;为Form对应的字段提供值。对于输入控件的抽象我认为这已经达到了极致&#xff0c;事件&#xff08;onChange&#xff09;产生值&#xff08;valu…