使用Spring和JWT的安全性

Introduction

我终于可以把这篇文章发表了。 花了这么长时间是因为两个主要原因。 首先,在过去的几周中,我陷入了一个非常有趣的项目。 当我们部署它,进行测试运行并看到所有利益相关者都对产品如何达到预期标准和所需功能感到满意时,感觉真的很好。 其次,令人遗憾的是,我的硬盘崩溃了,恢复过程相当艰巨。 (我仍在评估到目前为止丢失的内容,尤其是我的文章和文档)。 但是值得庆幸的是,我的大多数项目代码都在各自的git存储库中,与笔记本电脑上的内容相比,变化并不大。 这就是为什么我想借此机会再次提醒我们开发人员,我们应该始终尽可能频繁地将代码存储库推送到当前的工作分支。 即使任务没有完全完成,或者随着时间的推移,代码也没有编译。 它不必每天都在做,但至少要确保您在一周之内做出承诺并努力推动。 这种推动可能会使您在以后的日子里涌出。

Now that we have that out of the way, let's focus on what I really want to talk about in this article. In my last two articles here and here, I demonstrated on how to build REST APIs and integrate them with external APIs using Design patterns and best practices in developing applications. In this article I will be demonstrating on how to implement Security using JWT to secure our REST APIs. I will also highlight other strategies in securing our application especially with respect to data. I know this might be familiar topic, but they are usually written in isolation from the overall context of developing application. So we will be building on what we have developed in the previous articles I wrote. I will still be using the same approach in terms of class, component and method referencing so you might want to access the repo here and also take a look at the previous articles to play catch up a bit.

Code Implementation

假设您是一名顾问,并且您将要与您的新客户进行约会。 您知道演习将首先向客户注册为顾问,您将在其中提供所有详细信息,这些详细信息将保留在他们的记录中。 这类似于注册当您想首次使用应用程序时,我们会在线执行此过程。 注册后,您可以在约定的日期继续预约。 到达房屋后,您便会在接待处表明自己的身份,如果所有退房都将获得访客的标签或卡,则您可以进入建筑物的某些部分或楼层。 注册时,可以使用您的凭据将此过程映射到应用程序的登录过程。 这是认证方式流程,如果您的凭据有效,则将授予您对该应用程序的访问权限。 同时,还会向您发出JWT(令牌)(或在有状态的Web应用程序或浏览器的Cookie中为您生成会话)。 该令牌表示已发给您的访客标签。 就像标记一样,令牌将基于分配给令牌的角色,授予您对应用程序域中资源的受控访问权限。 这用于授权书 process to determine whether you can access a resource or not based on your role(s) (just as how the visitor's tag will only grant you access to certain floors in the building). Now just like the tag which can only be used for that day only, the token also has an expiration date and becomes invalid hence resulting in authorization failures and denial of access to resources. In some cases though, assuming you are not done with your consultation for that day, the tag can be reused for the next day based upon agreement. In the same vein, the token can also be refreshed upon request and a new expiration date is set for it after validating the old token. In the event, you are done with your consultation, you have to submit the tag before leaving the building. Likewise when you are done using the application, you need to logout so that the token is invalidated. And once you are out of the building or logged out of the application, you will have to repeat the identification process at the reception or the login process again to access the facility or the application in order to perform your activities. Now that we have a picture of how 认证方式, 授权书 and Token Generation and Management work together, Let's connect the dots using the Spring Security Framework and JWT:

首先,我们创建我们的JwtManager类,用于处理来自令牌的信息的生成,验证和检索。 然后我们创建我们的CustomUserDetails类。 此类将存储Spring认证用户或主要用户的详细信息。 通过实施org.springframework.security.core.userdetails.UserDetails界面,它可以访问信息,例如用户名,密码和通过用户分配的角色授予委托人的权限。 接下来我们创建我们的CustomUserDetailsS​​ervice类。 此类定义了loadUserByUsername中的方法org.springframework.security.core.userdetails.UserDetailsS​​ervice它实现的接口并根据结果返回我们先前定义的CustomUserDetails实例。 在这种情况下,我们将使用提供的用户名查询数据库,以查看其是否存在。

然后我们创建我们的JwtAuthenticationFilter类。 该类针对受保护的资源过滤每个传入请求,以检查和验证使用validateTokenJwtManager类中定义的方法。 如果有效,它将使用来从令牌声明中提取用户名(主题)和角色getUsernameFromJWT和getRolesFromJwt methods respectively that were defined in our JwtManager class和uses it to create a valid secuity context for the user so the user remains authenticated in the application. We could have equally used a second approach with just the username和the loadUserByUsername method of the CustomUserDetailsService class to achieve this with this code snippet

        String jwt = getJwtFromRequest(request);

            if (StringUtils.hasText(jwt) && jwtManager.validateToken(jwt)) {
                String username = jwtManager.getUsernameFromJWT(jwt);

        //Using an injected CustomUserDetailsService instance
        CustomUserDetails customUserDetails = customUserDetailsService.loadUserByUsername(username);
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(customUserDetails.getPrincipal(), "", customUserDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }else {
                SecurityContextHolder.getContext().setAuthentication(null);
            }

第二种方法的缺点是,对于每个请求,使用loadUserByUsername方法都会有一个性能数据库命中,这与第一种方法不同,在第一种方法中,我们通过从发送的令牌中检索用户的详细信息来提高应用程序的性能。 另一方面,第二种方法允许在更改用户角色时实时修改授予的权限,而第一种方法则要求令牌在生效之前过期。 您在此处采用的哪种方法通常都是基于设计和需求决策的。

接下来,我们将创建我们的CustomRestAccessDenied和CustomRestAuthenticationEntryPoint类。 如您所见,它们实现了org.springframework.security.web.access.AccessDeniedHandler和the org.springframework.security.web.AuthenticationEntryPoint接口。 的处理 method in the CustomRestAccessDenied class is meant to be triggered for requests where users have been authenticated but do not have the permissible role to access the resources和returns a HTTP 403 status while the 开始 method in the CustomRestAuthenticationEntryPoint class is triggered for unauthenticated users和returns a HTTP 401 status. You will see how this is setup when we look at our Security Configuration class implementation. Also note how the response is tailored to return in a particular format. This is so that we adhere to a consistent response structure in our application. For those who saw my last article on Exception handling, you maybe wondering why didn't we use that approach in this scenario. The reason for this is that the Access Denied和Unauthorized exceptions are thrown at an outer level than the Spring's ControllerAdvise和Exception can 处理 hence we have to 处理 these exceptions at the level which they occur.

现在让我们看一下我们的Security配置类安全配置 class. The 安全配置 class which extends org.springframework.security.config.annotation.web.configuration.Web安全配置urerAdapter到目前为止,我们已经定义了所有先前的安全组件和类。 它确定了它们在我们的应用程序安全实现中扮演的行为和角色。 的secureEnabled,jsr250Enabled和prePostEnabled注释使我们能够确定哪些角色应有权访问特定资源。 可以在要应用这些限制的方法上(例如,在控制器中)配置它,也可以将其附加到antMatchers as can be seen in the configure method of the 安全配置 class. The CustomUserDetailsService class we defined earlier is injected and specified to be used as our authentication component in the overridden 公共无效的配置(AuthenticationManagerBuilder authenticationManagerBuilder)方法。 我们还配置了密码编码器豆使用春天的BCryptPasswordEncoder也可以使用此方法来验证用户密码。 在被覆盖protected void configure(HttpSecurity httpSecurity)方法,我们为未授权和拒绝访问的场景配置了异常处理程序,以分别使用注入的CustomRestAuthenticationEntryPoint和CustomRestAccessDenied类。 我们禁用了csrf,以便来自其他域的请求可以访问我们的端点,然后我们声明会话管理为无状态的,从而符合REST规范。 然后,我们将注入的JwtAuthenticationFilter类配置为过滤器,该过滤器将针对对任何安全资源的每个请求进行调用。 最后,我们对/ api / auth /路由内的端点禁用安全性,因为未经身份验证的用户将在注册和登录时调用此安全性。

现在,让我们创建允许用户能够在我们的应用程序上注册和登录的组件。 首先,我们创建我们的用户将映射到我们的类实体使用者数据库中的表。 我们还创建了角色类实体也将映射到角色 table. We then insert into the 角色 table the following 角色: ROLE_USER和ROLE_ADMIN. Then we create the 用户Repository和用户Service classes for managing our 用户 entity和the 角色Repository和角色Service classes to manage our 角色 entity. For integrity, we create a 角色Type enum that maps to the 角色 we just created in our 角色 table. We then create our AuthController class to handle registration和login. As you can see, the registration endpoint is pretty straightforward. We create a user with the assigned 角色和store in the database. For the login endpoint, you supply a registered username和password和if authentication by the Spring Security framework is successful, a valid secuity context for the user is set using the Spring's 认证方式宾语。 的generateToken method of the JwtManager then takes in the authentication as a parameter和sets a Claims object's subject with the username和puts the authorities gotten from the Principal which were all gotten from the authentication object as 角色 into the Claims object. A token is then generated using the constructed Claims object和setting the expiry date, time it was issued和finally signed with our token secret key.

Test Run

现在已经全部设置好了,因此我们可以使用mvn spring-boot:run启动spring应用程序,然后尝试使用我们的终端命中端点进行注册注册请求dto类的结构:

    POST    http://localhost:8080/api/auth/register
    {

    "name":"user1",
    "username":"username1",
    "email":"username1@email.com",
    "password":"password1"
    }

       Response:
    {
        "status": 0,
        "message": "User registered successfully",
        "result": null
    }

对其他用户重复上述操作,以创建和管理

    POST    http://localhost:8080/api/auth/register/admin
    {

    "name":"user2",
    "username":"username2",
    "email":"username2@email.com",
    "password":"password2"
    }

        Response:
    {
        "status": 0,
        "message": "Admin registered successfully",
        "result": null
    }

使用我们的两个用户登录登录请求dto类结构并获取将为两个用户返回的相应令牌

    POST http://localhost:8080/api/auth/login

    {
    "username":"username1",
    "password":"password1"
    }

        Response:
    {
        "status": 0,
        "message": "Token generated successfully",
        "result": {
        "accessToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VybmFtZTEiLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTU2MDk4MDU1MywiZXhwIjoxNTYwOTgyMzUzfQ.boqHQh3gLPgNWBP0GiATBGg-25bwMfg33-zn5BFK9AiFuhYcWcmSSFp_isjlhL_xJ9WxMW6A7UWaVOXMdx94ng"
        }
    }

调用没有令牌或无效或过期令牌的任何端点将给您一个由我们的CustomRestAuthenticationEntryPoint处理程序触发的响应,该响应程序带有HTTP 401状态代码

    {
        "timestamp": "2019-06-19T22:51:37.189",
        "message": "Unauthorized user",
        "status": 401
    }

呼叫具有未授权角色的端点时,请说

http://localhost:8080/card-scheme/stats?start=1&limit=10

使用user1生成的令牌时,我们的CustomRestAccessDenied处理程序将触发响应,并带有HTTP 403状态代码,因为只有管理员角色才能访问它。

    {
        "timestamp": "2019-06-19T22:51:57.213",
        "message": "Access Denied",
        "status": 403
    }

Unit Testing

现在让我们看一下如何设置编写测试的方法。 因为我们关注身份验证和授权,所以我们将限制我们的测试范围,使其不超出控制器层,因为这是这些限制所在。 为此,我创建了安全测试类,并用@WebMvcTest注解。 然后,我嘲笑了控制器层所需的所有依赖关系,但我所侦察的JwtManager除外。 原因是我将在测试中实际上将在JwtManager类中调用真实方法以进行令牌生成和验证。 然后我创建AppTestConfig配置类并在控制器层设置基本程序包扫描。 但是我必须创建一个EntityManagerFactory将使用内存中的H2数据库进行启动和引导的bean。 然后,在我的setUp方法中,添加了JwtAuthenticationFilter实例作为用于验证令牌的过滤器。 最后,我使用.apply(springSecurity())。 运行测试将为我们提供测试课程中定义的预期结果。

在总结本文之前,让我们先讨论保护应用程序安全的其他方面。

Data Encryption

数据加密已经存在了很长时间,直到人们能够进行通信为止。 我不会详细介绍这些证据,但是数据加密几乎与个人或当事方之间的信息交换同样重要。 数据加密只是使信息对无意接收者无用或含糊不清的一种行为,除了预期的接收者之外。

在当今世界,数据加密基于两种不同形式的加密:

  1. 对称密钥非对称密钥。

对称密钥形式使用AES该算法使用单个密钥进行加密和解密,并由相关各方共享。 另一方面,使用RSA该算法涉及两个密钥,公钥和私钥。 公开密钥可以与任何人共享,但是私有密钥必须保持秘密。 两者都可以用于加密消息,然后使用与最初用于加密该消息的密钥相反的密钥对消息进行解码。 还有一个对加密数据进行签名的概念,对于不可否认。 这涉及使用私钥对加密的文件进行签名,然后使用公钥来验证签名。

尽管在我们的应用程序中实际上没有用例来实现任何一种加密形式,但是您可以看一下RSA加密,AES加密和加密和签名测试 classes I created to see the available utility methods和how they are used to perform various operations. Notice how in the RSA加密 class how you can either generate keys dynamically by calling the generateKeyPair方法或通过调用以下命令从密钥库文件加载getKeyPairFromKeyStore method. The keystore file can be generated using the Java keytool utility. One guiding rule in determining which form to use (RSA or AES) is the size of the data to encrypt. For large amounts of data,it is advised to use the AES form as it's faster和less computationally intensive while the RSA can be used for smaller amounts of data.

数据加密既可以在静止状态下也可以在运动中进行。

运动加密中数据的常见示例是使用HTTPS协议。 这里发生的是,当浏览器向SSL服务器发出请求时,它会获取公共密钥并生成一个基于随机数的密钥。 然后,它使用公共密钥对生成的密钥进行加密,并将其发送回服务器。 然后,服务器使用私钥解密并检索生成的密钥。 交换信息时,正是此生成的密钥供服务器和浏览器用于加密和解密。 这样做的好处是,只有浏览器会话和服务器才知道用于信息交换的生成密钥。

对于非浏览器应用程序,我们也可以模拟上述情况,从而生成密钥对(使用我刚才提到的加密类的公钥和私钥)。 然后,所涉及的应用程序交换其公钥,并被用于加密和验证签名,而私钥则被用于解密和签名数据。

因此,在我们这里的情况下,我们可以通过利用HTTPS协议上的应用程序来同样地为运动中的数据实现数据加密。 因此,我们的解决方案将更多是一种体系结构的实现,而不是编码的实现。

在静态数据加密的情况下,这通常涉及保护数据存储组件或系统(例如数据库(RDBMS,NOSQL甚至是缓存系统))中的数据,甚至是配置文件值。 换句话说,使用我之前创建的加密实用程序类,我们可以将数据存储为加密格式,并且仅在将要在应用程序中使用时解密。 如果落入错误的人手中,这种方法会使我们的数据存储区中的数据变得晦涩难懂。 我们也可以选择对数据进行加密,而不必期望将其解密,只要我们首先知道用于加密数据的算法即可。 这比加密要安全得多,它被称为散列。

此处哈希的一个经典示例是将用户密码存储在我们的数据库中(最近有人强烈要求Facebook以纯文本格式存储Instagram用户的密码)。 我们在这里使用了单向哈希实现。 换句话说,与之前讨论的加密技术不同,在对其进行哈希处理之后,无法对其进行解密(或在实践中是不可能的)。 此处的原理是,使用特定算法对特定字符串序列进行哈希运算将始终生成相同的加密字符串(关于Salting的情况除外,我将在稍后讨论)。 在这里,我们使用了BCryptPasswordEncoder编码可以在我们的SecurityConfig类中看到的方法来实现此目的,在该类中它被用作PasswordEncoder,并在注册过程中创建用户时使用。

仅仅对我们的密码进行哈希处理并不能确保其安全。 Rainbow表的可用性已证明是事实。 这些工具的在线可用性使破解密码成为黑客的嗜好,尤其是非常简单和常见的密码,例如流行词,连续的字母和数字,甚至是个人信息(例如姓名,日期等)。这导致了被称为盐. 盐 is a technique whereby random sequence of characters are added as part of the parameters required for password hashing. It makes using rainbow tables very difficult to use as even the simple password instances mentioned above will no longer have entries in these tables as a result of salting.

在较旧的实现中,Spring提供了MessageDigestPasswordEncoder该类允许我们提供自己的盐字符串以哈希密码。 一种有效的用法是为每个要哈希的密码字符串提供不同的盐字符串(例如,注册时的时间以毫秒为单位)。 但是,不推荐使用此方法,而推荐使用我们当前在应用程序中使用的BCryptPasswordEncoder。 BCryptPasswordEncoder不仅对我们的密码进行哈希处理,而且还会在内部为我们生成一个随机的字符串,从内部实现可以看出。 此实现为我们减轻了管理盐字符串生成的负担,因为这对于我们密码的完整性至关重要。 结果,每次您对相同的密码字符串进行哈希处理时,都会产生一个不同的哈希字符串。 现在出现了问题,我们如何确定比较和匹配密码? BCryptPasswordEncoder实现将哈希字符串分解为salt字符串,cost参数和哈希值,所有这些都可以从哈希字符串中获取,并且由于salt确定了生成的哈希,因此可以用来确定所提供的密码是否匹配或匹配。 与我们所拥有的不一样。

最后,我想谈谈保护我们的应用程序配置文件的安全,该文件通常名为application.properties文件或application.yaml和我们一样 尽管在我们的案例中,您会注意到当前正在从环境变量中检索诸如数据库凭据之类的敏感信息,但我已经看到了将这些详细信息以纯文本格式存储在文件中的情况。 即使使用环境变量在一定程度上减轻了将其作为纯文本存储在文件中的风险,但另一种方法是使用Jasypt Spring启动启动器模块。 这个模块允许我们以加密形式存储值,然后解密以在运行时使用。

要在我们的应用程序中实现此功能,我们首先需要添加jasypt-spring-boot-starter给我们pom.xml

<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>

然后我们运行下面的命令

java -cp $M2_REPO/org/jasypt/jasypt/1.9.2/jasypt-1.9.2.jar  org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="yourpassword" password=jasypt_password algorithm=PBEWithMD5AndDES

哪里

  • $ M2_REPO是您的Maven存储库目录输入参数是您需要加密的密码,例如您的数据库密码密码 argument is the Jasypt 密码 or key for encryption you will be supplying when starting up your application算法 is the Password Based Encryption (PBE) 算法 of your choice provided by Jasypt (PBEWITHMD5ANDDES,PBEWITHMD5ANDTRIPLEDES,PBEWITHSHA1ANDDESEDE,PBEWITHSHA1ANDRC2_40)

上面的命令将生成您提供的输入参数密码值的加密值。 您可以对application.properties或yaml文件中希望加密的任何其他值重复该命令。

然后,您可以使用以下格式的这些加密值来设置属性或环境变量值ENC(加密值)例如在我们的Yaml中,您可以像这样更改密码值

.
.
.
spring:
  profiles: development
  application:
    name: binList
  datasource:
     password: ENC(encrypted_password_from_jasypt_operation)
     .
     .
     .

或者您只是将yaml文件保留为原样,并使用它来设置SPRING_DATASOURCE_PASSWORD环境变量

export SPRING_DATASOURCE_PASSWORD=ENC(encrypted_password_from_jasypt_operation)

然后您可以像这样启动您的应用程序

mvn -Djasypt.encryptor.password=jasypt_password spring-boot:run

或者您导出JASYPT_ENCRYPTOR_PASSWORD环境变量,然后只运行mvn spring-boot:run

export JASYPT_ENCRYPTOR_PASSWORD=jasypt_password

为了进一步加强我们应用服务器上的安全性,您可以按顺序执行以下步骤

  1. 使用批处理脚本(.sh或.bat,取决于您的操作系统)导出并设置前面提到的所有环境变量Start up the application as a background service like this mvn spring-boot:run & or as a Windows Service使用批处理脚本取消设置所有环境变量,因为在应用程序启动时仅一次请求这些值。只要您在其他地方有副本,请删除批处理脚本。

这些步骤的原因是为了防止用户使用ps和history命令查询您的应用程序服务器,以查看先前运行的命令中的密码或环境变量。

So there you have it. We have secured our application although as we all know security can never be total but it should slow down and require more effort for someone to maliciously use our application. There are other security strategies I have left out here especially at Data Access Object layer such as query input parameterization for SQL injection due to lack of SQL or JQL implementations in our application. You can access the updated source code for this article here.

在我的下一篇文章中,如果时间允许,我将演示如何实现可扩展性和可用性与我们的应用程序一起使用码头工人和some code refactorings. Also I hope to recover the Laravel和Node(NestJs) implementations of what we have done so far from my laptop crash和share it. That's all folks! Happy Coding.

from: https://dev.to//charles1303/security-using-spring-and-jwt-aai

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值