إنه ليس بديلاً عن Spring Security ، ولكنه يعمل بشكل جيد في الإنتاج لأكثر من عامين حتى الآن.
سأحاول وصف العملية برمتها بأكبر قدر ممكن من التفاصيل ، من إنشاء مفتاح لـ JWT إلى وحدة تحكم ، حتى يتمكن أي شخص غير مطلع على JWT من فهم كل شيء.
المحتوى
- خلفية
- توليد المفتاح
- إنشاء مشروع الربيع
- TokenHandler
- الشرح والمعالج
- معالجة استثناء المصادقة
- مراقب
0. الخلفية
بادئ ذي بدء ، أريد أن أخبرك ما الذي دفعني بالضبط لتنفيذ طريقة مصادقة العميل هذه ولماذا لم أستخدم Spring Security. إذا لم تكن مهتمًا ، يمكنك الانتقال إلى الفصل التالي.
بحلول ذلك الوقت ، كنت أعمل في شركة صغيرة تقوم بتطوير مواقع الويب. كانت هذه أول وظيفة لي في هذا المجال ، لذا لم أكن أعرف شيئًا حقًا. بعد حوالي شهر من العمل ، قالوا إنه سيكون هناك مشروع جديد وأنه من الضروري إعداد الوظائف الأساسية له. قررت أن أرى بمزيد من التفصيل كيف تم تنفيذ هذه العملية في المشاريع القائمة. للأسف ، لم يكن كل شيء سعيدًا هناك.
في كل طريقة لوحدة التحكم ، حيث كان من الضروري سحب المستخدم المصرح له ، كان هناك شيء مشابه لما يلي
@RequestMapping(value = "/endpoint", method = RequestMethod.GET)
public Response endpoint() {
User user = getUser(); //
if (null == user)
return new ErrorResponse.Builder(Error.AUTHENTICATION_ERROR).build();
//
}
وهكذا كان في كل مكان ... بدأت إضافة نقطة نهاية جديدة بحقيقة أن هذا الجزء من الكود قد تم نسخه. لقد وجدت أنه غريب
لحل هذه المشكلة ، ذهبت إلى google. ربما كنت أبحث عن شيء خاطئ ، لكنني لم أجد الحل المناسب. كانت تعليمات تكوين Spring Security موجودة في كل مكان.
اسمحوا لي أن أشرح لماذا لم أرغب في استخدام Spring Security. بدا لي أنه معقد للغاية وغير مناسب جدًا لاستخدامه في REST. نعم ، وفي طرق معالجة نقطة النهاية ، ربما لا يزال يتعين عليك إخراج المستخدم من السياق. ربما أكون مخطئًا ، لأنني لم أكن أعرف الكثير عنها ، لكن المقال لا يتعلق بذلك على أي حال.
كنت بحاجة إلى شيء بسيط وسهل الاستخدام. جاءت الفكرة للقيام بذلك من خلال التعليق التوضيحي.
الفكرة هي أننا نقوم بحقن مستخدمنا في كل طريقة من طرق وحدة التحكم ، حيث يلزم الحصول على إذن. و هذا كل شيء. اتضح أنه داخل طريقة وحدة التحكم سيكون هناك بالفعل مستخدم مصرح له وسيكون ! = Null (باستثناء الحالات التي لا يكون فيها التفويض مطلوبًا).
اكتشفنا أسباب إنشاء هذه الدراجة. الآن دعنا نبدأ في التدريب.
1. مفتاح الجيل
أولاً ، نحتاج إلى إنشاء مفتاح يقوم بتشفير الحد الأدنى من المعلومات المطلوبة حول المستخدم.
توجد مكتبة ملائمة جدًا للعمل في java مع jwt .
يحتوي github على جميع الإرشادات حول كيفية العمل مع jwt ، ولكن لتبسيط العملية ، سأقدم مثالاً أدناه.
لإنشاء المفتاح ، قم بإنشاء مشروع عادي وأضف التبعيات التالية
التبعيات
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
والطبقة التي ستولد السر
SecretGenerator.java
package jwt;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
public class SecretGenerator {
public static void main(String[] args) {
SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);
String secretString = Encoders.BASE64.encode(secretKey.getEncoded());
System.out.println(secretString);
}
}
نتيجة لذلك ، نحصل على مفتاح سري ، سنستخدمه في المستقبل.
2. إنشاء مشروع الربيع
لن أصف عملية الإنشاء ، نظرًا لوجود العديد من المقالات والبرامج التعليمية حول هذا الموضوع. وعلى موقع Spring الرسمي ، يوجد مُهيئ ، حيث يمكنك إنشاء مشروع صغير بنقرتين.
سأترك فقط ملف بوم النهائي
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
</parent>
<groupId>org.website</groupId>
<artifactId>backend</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<java.version>14</java.version>
<start-class>org.website.BackendWebsiteApplication</start-class>
</properties>
<profiles>
<profile>
<id>local</id>
<properties>
<activatedProperties>local</activatedProperties>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<!--*******SPRING*******-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--*******JWT*******-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<!--*******OTHER*******-->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.14</version>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<!--*******TEST*******-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
بعد إنشاء المشروع ، انسخ المفتاح الذي تم إنشاؤه مسبقًا إلى application.properties
app.api.jwtEncodedSecretKey=teTN1EmB5XADI5iV4daGVAQhBlTwLMAE+LlXZp1JPI2PoQOpgVksRqe79EGOc5opg+AmxOOmyk8q1RbfSWcOyg==
3. TokenHandler
سنحتاج إلى خدمة لإنشاء الرموز وفك تشفيرها.
سيحتوي الرمز المميز على حد أدنى من المعلومات حول المستخدم (معرفه فقط) ووقت انتهاء صلاحية الرمز المميز. لهذا ، سنقوم بإنشاء واجهات.
لنقل عمر الرمز المميز.
expiration.java
package org.website.jwt;
import java.time.LocalDateTime;
import java.util.Optional;
public interface Expiration {
Optional<LocalDateTime> getAuthTokenExpire();
}
ونقل الهوية. سيتم تنفيذه من قبل كيان المستخدم
CreateBy.java
package org.website.jwt;
public interface CreateBy {
Long getId();
}
سننشئ أيضًا تطبيقًا افتراضيًا لواجهة انتهاء الصلاحية . افتراضيًا ، سيعيش الرمز المميز لمدة 24 ساعة.
DefaultExpiration.java
package org.website.jwt;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Optional;
@Component
public class DefaultExpiration implements Expiration {
@Override
public Optional<LocalDateTime> getAuthTokenExpire() {
return Optional.of(LocalDateTime.now().plusHours(24));
}
}
دعنا نضيف فصلين من الفئات المساعدة.
GeneratedTokenInfo - للحصول على معلومات حول الرمز الذي تم إنشاؤه.
TokenInfo - للحصول على معلومات حول الرمز المميز الذي جاء إلينا.
GeneratedTokenInfo.java
package org.website.jwt;
import java.time.LocalDateTime;
import java.util.Optional;
public class GeneratedTokenInfo {
private final String token;
private final LocalDateTime expiration;
public GeneratedTokenInfo(String token, LocalDateTime expiration) {
this.token = token;
this.expiration = expiration;
}
public String getToken() {
return token;
}
public LocalDateTime getExpiration() {
return expiration;
}
public Optional<String> getSignature() {
if (null != this.token && this.token.length() >= 3)
return Optional.of(this.token.split("\\.")[2]);
return Optional.empty();
}
}
TokenInfo.java
package org.website.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import lombok.NonNull;
import java.time.LocalDateTime;
import java.time.ZoneId;
public class TokenInfo {
private final Jws<Claims> claimsJws;
private final String signature;
private final Claims body;
private final Long userId;
private final LocalDateTime expiration;
private TokenInfo() {
throw new UnsupportedOperationException();
}
private TokenInfo(@NonNull final Jws<Claims> claimsJws,
@NonNull final String signature,
@NonNull final Claims body,
@NonNull final Long userId,
@NonNull final LocalDateTime expiration) {
this.claimsJws = claimsJws;
this.signature = signature;
this.body = body;
this.userId = userId;
this.expiration = expiration;
}
public static TokenInfo fromClaimsJws(@NonNull final Jws<Claims> claimsJws) {
final Claims body = claimsJws.getBody();
return new TokenInfo(
claimsJws,
claimsJws.getSignature(),
body,
Long.parseLong(body.getId()),
body.getExpiration().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
}
public Jws<Claims> getClaimsJws() {
return claimsJws;
}
public String getSignature() {
return signature;
}
public Claims getBody() {
return body;
}
public Long getUserId() {
return userId;
}
public LocalDateTime getExpiration() {
return expiration;
}
}
الآن TokenHandler نفسه . سيُنشئ رمزًا مميزًا بناءً على إذن المستخدم ، بالإضافة إلى استرداد معلومات حول الرمز الذي جاء به المستخدم المصرح له مسبقًا.
TokenHandler.java
package org.website.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.sql.Date;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
import java.util.Optional;
@Service
@Slf4j
public class TokenHandler {
@Value("${app.api.jwtEncodedSecretKey}")
private String jwtEncodedSecretKey;
private final DefaultExpiration defaultExpiration;
private SecretKey secretKey;
@Autowired
public TokenHandler(final DefaultExpiration defaultExpiration) {
this.defaultExpiration = defaultExpiration;
}
@PostConstruct
private void postConstruct() {
byte[] decode = Base64.getDecoder().decode(jwtEncodedSecretKey);
this.secretKey = new SecretKeySpec(decode, 0, decode.length, "HmacSHA512");
}
public Optional<GeneratedTokenInfo> generateToken(CreateBy createBy, Expiration expire) {
if (null == expire || expire.getAuthTokenExpire().isEmpty())
expire = this.defaultExpiration;
try {
final LocalDateTime expireDateTime = expire.getAuthTokenExpire().get().withNano(0);
String compact = Jwts.builder()
.setId(String.valueOf(createBy.getId()))
.setExpiration(Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant()))
.signWith(this.secretKey)
.compact();
return Optional.of(new GeneratedTokenInfo(compact, expireDateTime));
} catch (Exception e) {
log.error("Error generate new token. CreateByID: {}; Message: {}", createBy.getId(), e.getMessage());
}
return Optional.empty();
}
public Optional<GeneratedTokenInfo> generateToken(CreateBy createBy) {
return this.generateToken(createBy, this.defaultExpiration);
}
public Optional<TokenInfo> extractTokenInfo(final String token) {
try {
Jws<Claims> claimsJws = Jwts.parserBuilder()
.setSigningKey(this.secretKey)
.build()
.parseClaimsJws(token);
return Optional.ofNullable(claimsJws).map(TokenInfo::fromClaimsJws);
} catch (Exception e) {
log.error("Error extract token info. Message: {}", e.getMessage());
}
return Optional.empty();
}
}
لن ألفت انتباهكم ، لأن كل شيء يجب أن يكون واضحًا في هذا الأمر.
4. الشرح والمعالج
لذلك ، بعد كل الأعمال التحضيرية ، دعنا ننتقل إلى الأكثر إثارة للاهتمام. كما ذكرنا سابقًا ، نحتاج إلى تعليق توضيحي يتم إدخاله في طرق وحدة التحكم ، حيث يلزم وجود مستخدم مرخص.
قم بإنشاء تعليق توضيحي باستخدام الكود التالي
AuthUser.java
package org.website.annotation;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthUser {
boolean required() default true;
}
قيل سابقًا أن الإذن قد يكون اختياريًا. لهذا فقط ونحتاج إلى طريقة مطلوبة في الملخص. إذا كان الترخيص لطريقة معينة اختياريًا وإذا كان المستخدم الوارد غير مصرح له حقًا ، فسيتم إدخال قيمة null في الطريقة . لكننا سنكون مستعدين لذلك.
تم إنشاء التعليق التوضيحي ، لكننا ما زلنا بحاجة إلى معالج ، والذي سيحصل على الرمز المميز من الطلب ، ويستقبله من قاعدة المستخدمين ويعيد توجيهه إلى طريقة وحدة التحكم. لمثل هذه الحالات ، يحتوي Spring على واجهة HandlerMethodArgumentResolver . سوف ننفذها.
قم بإنشاء فئة AuthUserHandlerMethodArgumentResolver التي تنفذ الواجهة أعلاه.
AuthUserHandlerMethodArgumentResolver.java
package org.website.annotation.handler;
import org.springframework.core.MethodParameter;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.util.WebUtils;
import org.website.annotation.AuthUser;
import org.website.annotation.exception.AuthenticationException;
import org.website.domain.User;
import org.website.domain.UserJwtSignature;
import org.website.jwt.TokenHandler;
import org.website.service.repository.UserJwtSignatureService;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
import java.util.Optional;
public class AuthUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
private final String AUTH_COOKIE_NAME;
private final String AUTH_HEADER_NAME;
private final TokenHandler tokenHandler;
private final UserJwtSignatureService userJwtSignatureService;
public AuthUserHandlerMethodArgumentResolver(final String authTokenCookieName,
final String authTokenHeaderName,
final TokenHandler tokenHandler,
final UserJwtSignatureService userJwtSignatureService) {
this.AUTH_COOKIE_NAME = authTokenCookieName;
this.AUTH_HEADER_NAME = authTokenHeaderName;
this.tokenHandler = tokenHandler;
this.userJwtSignatureService = userJwtSignatureService;
}
@Override
public boolean supportsParameter(@NonNull final MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(AuthUser.class) != null && methodParameter.getParameterType().equals(User.class);
}
@Override
public Object resolveArgument(@NonNull final MethodParameter methodParameter,
final ModelAndViewContainer modelAndViewContainer,
@NonNull final NativeWebRequest nativeWebRequest,
final WebDataBinderFactory webDataBinderFactory) throws Exception {
if (!this.supportsParameter(methodParameter))
return WebArgumentResolver.UNRESOLVED;
// required
final boolean required = Objects.requireNonNull(methodParameter.getParameterAnnotation(AuthUser.class)).required();
// HttpServletRequest
Optional<HttpServletRequest> httpServletRequestOptional = Optional.ofNullable(nativeWebRequest.getNativeRequest(HttpServletRequest.class));
//
Optional<UserJwtSignature> userJwtSignature =
this.extractAuthTokenFromRequest(nativeWebRequest, httpServletRequestOptional.orElse(null))
.flatMap(tokenHandler::extractTokenInfo)
.flatMap(userJwtSignatureService::extractByTokenInfo);
if (required) {
//
if (userJwtSignature.isEmpty() || null == userJwtSignature.get().getUser())
//
throw new AuthenticationException(httpServletRequestOptional.map(HttpServletRequest::getMethod).orElse(null),
httpServletRequestOptional.map(HttpServletRequest::getRequestURI).orElse(null));
final User user = userJwtSignature.get().getUser();
//
return this.appendCurrentSignature(user, userJwtSignature.get());
} else {
// , , null
return this.appendCurrentSignature(userJwtSignature.map(UserJwtSignature::getUser).orElse(null),
userJwtSignature.orElse(null));
}
}
private User appendCurrentSignature(User user, UserJwtSignature userJwtSignature) {
Optional.ofNullable(user).ifPresent(u -> u.setCurrentSignature(userJwtSignature));
return user;
}
private Optional<String> extractAuthTokenFromRequest(@NonNull final NativeWebRequest nativeWebRequest,
final HttpServletRequest httpServletRequest) {
return Optional.ofNullable(httpServletRequest)
.flatMap(this::extractAuthTokenFromRequestByCookie)
.or(() -> this.extractAuthTokenFromRequestByHeader(nativeWebRequest));
}
private Optional<String> extractAuthTokenFromRequestByCookie(final HttpServletRequest httpServletRequest) {
return Optional
.ofNullable(httpServletRequest)
.map(request -> WebUtils.getCookie(httpServletRequest, AUTH_COOKIE_NAME))
.map(Cookie::getValue);
}
private Optional<String> extractAuthTokenFromRequestByHeader(@NonNull final NativeWebRequest nativeWebRequest) {
return Optional.ofNullable(nativeWebRequest.getHeader(AUTH_HEADER_NAME));
}
}
في المُنشئ ، نقبل أسماء ملف تعريف الارتباط والرأس الذي يمكن تمرير الرمز فيه. لقد أخرجتهم في application.properties
app.api.tokenKeyName=Auth-Token
app.api.tokenHeaderName=Auth-Token
يتم أيضًا تمرير TokenHandler و UserJwtSignatureService الذي تم إنشاؤه مسبقًا في المُنشئ . لن نأخذ في الاعتبار UserJwtSignatureService ، نظرًا لوجود استخراج قياسي للمستخدم من قاعدة البيانات من خلال معرفه وتوقيع الرمز المميز. لكن دعنا نحلل كود المعالج نفسه بمزيد من التفصيل. supportsParameter - الشيكات إذا كانت طريقة يلبي متطلبات المطلوبة. ResolutionArgument هي الطريقة الرئيسية التي يحدث فيها كل "السحر". إذن ما الذي يحدث هنا:
- نحصل على قيمة الحقل المطلوبة من التعليق التوضيحي الخاص بنا
- HttpServletRequest
- ,
- required, , .
, , ( , ).
, , , . - , required, , null
تم إنشاء معالج التعليقات التوضيحية. لكن هذا ليس كل شيء. يجب أن تكون مسجلة لفصل الربيع لمعرفة ذلك. كل شيء بسيط هنا. قم بإنشاء ملف تكوين يقوم بتطبيق واجهة Spring's WebMvcConfigurer وتجاوز طريقة addArgumentResolvers
WebMvcConfig.java
package org.website.configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.website.annotation.handler.AuthUserHandlerMethodArgumentResolver;
import org.website.jwt.TokenHandler;
import org.website.service.repository.UserJwtSignatureService;
import java.util.List;
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${app.api.tokenKeyName}")
private String tokenKeyName;
@Value("${app.api.tokenHeaderName}")
private String tokenHeaderName;
private final TokenHandler tokenHandler;
private final UserJwtSignatureService userJwtSignatureService;
@Autowired
public WebMvcConfig(final TokenHandler tokenHandler,
final UserJwtSignatureService userJwtSignatureService) {
this.tokenHandler = tokenHandler;
this.userJwtSignatureService = userJwtSignatureService;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthUserHandlerMethodArgumentResolver(
this.tokenKeyName,
this.tokenHeaderName,
this.tokenHandler,
this.userJwtSignatureService));
}
}
بهذا تنتهي كتابة التعليق التوضيحي.
5. معالجة استثناء المصادقة
في القسم السابق ، في معالج التعليقات التوضيحية ، إذا كان التفويض مطلوبًا لطريقة وحدة التحكم ، ولكن المستخدم غير مصرح له ، فقد ألقينا استثناء المصادقة .
نحتاج الآن إلى إضافة فئة هذا الاستثناء والتعامل معه لإعادة json إلى المستخدم بالمعلومات التي نحتاجها.
AuthenticationException.java
package org.website.annotation.exception;
public class AuthenticationException extends Exception {
public AuthenticationException(String requestMethod, String url) {
super(String.format("%s - %s", requestMethod, url));
}
}
والآن معالج الاستثناء نفسه. من أجل التعامل مع الاستثناءات التي حدثت وإعطاء المستخدم ليس صفحة خطأ الربيع القياسية ، ولكن json الذي نحتاجه ، يحتوي Spring على تعليق توضيحي لـ ControllerAdvice .
دعونا نضيف فصلًا للتعامل مع التنفيذ.
AuthenticationExceptionControllerAdvice.java
package org.website.controller.exception.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.website.annotation.exception.AuthenticationException;
import org.website.http.response.Error;
import org.website.http.response.ErrorResponse;
import org.website.http.response.Response;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
@ControllerAdvice
@Slf4j
public class AuthenticationExceptionControllerAdvice extends AbstractControllerAdvice {
@Value("${app.api.tokenKeyName}")
private String tokenKeyName;
@ExceptionHandler({AuthenticationException.class})
public Response authenticationException(HttpServletResponse response) {
Cookie cookie = new Cookie(tokenKeyName, "");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
return new ErrorResponse.Builder(Error.AUTHENTICATION_ERROR).build();
}
}
الآن ، إذا تم طرح استثناء المصادقة ، فسيتم اكتشافه وسيتم إرجاع json به خطأ AUTHENTICATION_ERROR إلى المستخدم
6. تحكم
الآن ، في الواقع ، من أجل كل شيء بدأ. لنقم بإنشاء وحدة تحكم بثلاث طرق:
- إذن إلزامي
- مع أي ترخيص إجباري
- تسجيل مستخدم جديد. رمز الحد الأدنى. يقوم فقط بحفظ المستخدم في قاعدة البيانات ، بدون كلمات مرور. والذي سيعيد أيضًا رمز المستخدم الجديد
TestAuthController.java
package org.website.controller;
import com.google.gson.JsonObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.website.annotation.AuthUser;
import org.website.domain.User;
import org.website.http.response.Response;
import org.website.http.response.SuccessResponse;
import org.website.jwt.GeneratedTokenInfo;
import org.website.service.repository.UserJwtSignatureService;
import org.website.service.repository.UserService;
import java.util.Optional;
@RestController
@RequestMapping("/test-auth")
public class TestAuthController {
@Autowired
private UserService userService;
@Autowired
private UserJwtSignatureService userJwtSignatureService;
@RequestMapping(value = "/required", method = RequestMethod.GET)
public Response required(@AuthUser final User user) {
return new SuccessResponse.Builder(user).build();
}
@RequestMapping(value = "/not-required", method = RequestMethod.GET)
public Response notRequired(@AuthUser(required = false) final User user) {
JsonObject response = new JsonObject();
if (null == user) {
response.addProperty("message", "Hello guest!");
} else {
response.addProperty("message", "Hello " + user.getFirstName());
}
return new SuccessResponse.Builder(response).build();
}
@RequestMapping(value = "/sign-up", method = RequestMethod.GET)
public Response signUp(@RequestParam String firstName) {
User user = userService.save(User.builder().firstName(firstName).build());
Optional<GeneratedTokenInfo> generatedTokenInfoOptional =
userJwtSignatureService.generateNewTokenAndSaveToDb(user);
return new SuccessResponse.Builder(user)
.addPropertyToPayload("token", generatedTokenInfoOptional.get().getToken())
.build();
}
}
في الطرق المطلوبة وغير المطلوبة ، نقوم بإدخال التعليق التوضيحي الخاص بنا.
في الحالة الأولى ، إذا لم يكن المستخدم مصرحًا له ، فيجب إرجاع json بخطأ ، وإذا كان مصرحًا به ، فسيتم إرجاع معلومات حول المستخدم.
في الحالة الثانية ، إذا لم يقم المستخدم بتسجيل الدخول ، ستظهر رسالة Hello guest! وإذا تم التصريح بذلك ، فسيتم إرجاع اسمه.
دعونا نتحقق من أن كل شيء يعمل حقًا.
أولاً ، دعنا نتحقق من كلتا الطريقتين كمستخدم غير مصرح به.
كل شيء كما هو متوقع. عندما كان التفويض مطلوبًا ، تم إرجاع خطأ ، وفي الحالة الثانية ، الرسالة مرحبًا ضيف! ...
الآن دعنا نسجل ونحاول الاتصال بنفس الطرق ، ولكن مع نقل الرمز المميز في رؤوس الطلب.
أعاد الرد رمزًا مميزًا يمكن استخدامه لتلك الطلبات التي تتطلب التفويض.
دعنا نتحقق من هذا:
في الحالة الأولى ، يتم إرجاع معلومات عن المستخدم فقط. في الحالة الثانية ، يتم إرجاع رسالة ترحيب.
العمل!
7. الخاتمة
هذه الطريقة لا تدعي أنها الحل الصحيح الوحيد. قد يفضل شخص ما استخدام Spring Security. ولكن ، كما ذكرنا في البداية ، هذه الطريقة مجربة وسهلة الاستخدام وتعمل بشكل جيد للغاية.




