Ini bukan pengganti Keamanan Musim Semi, tetapi telah berproduksi dengan baik selama lebih dari dua tahun sekarang.
Saya akan mencoba mendeskripsikan keseluruhan proses sedetail mungkin, dari menghasilkan kunci untuk JWT hingga pengontrol, sehingga bahkan seseorang yang tidak terbiasa dengan JWT akan memahami semuanya.
Kandungan
- Latar Belakang
- Pembuatan kunci
- Pembuatan proyek musim semi
- TokenHandler
- Anotasi dan penangan
- Menangani AuthenticationException
- Kontroler
0. Latar Belakang
Pertama-tama, saya ingin memberi tahu Anda apa sebenarnya yang mendorong saya untuk menerapkan metode otentikasi klien ini dan mengapa saya tidak menggunakan Spring Security. Jika Anda tidak tertarik, Anda dapat langsung ke bab berikutnya.
Saat itu, saya bekerja di sebuah perusahaan kecil yang mengembangkan situs web. Ini adalah pekerjaan pertama saya di bidang ini, jadi saya tidak tahu apa-apa. Setelah sekitar sebulan bekerja, mereka mengatakan bahwa akan ada proyek baru dan perlu menyiapkan fungsionalitas dasar untuk itu. Saya memutuskan untuk melihat lebih detail bagaimana proses ini diimplementasikan dalam proyek yang sudah ada. Saya menyesal, semuanya tidak begitu bahagia di sana.
Di setiap metode pengontrol, di mana perlu untuk menarik pengguna yang sah, ada sesuatu seperti berikut ini
@RequestMapping(value = "/endpoint", method = RequestMethod.GET)
public Response endpoint() {
User user = getUser(); //
if (null == user)
return new ErrorResponse.Builder(Error.AUTHENTICATION_ERROR).build();
//
}
Dan begitulah di mana-mana ... Menambahkan titik akhir baru dimulai dengan fakta bahwa potongan kode ini telah disalin. Saya merasa
Untuk mengatasi masalah ini, saya membuka google. Mungkin saya sedang mencari sesuatu yang salah, tetapi saya tidak dapat menemukan solusi yang cocok. Petunjuk untuk mengonfigurasi Keamanan Musim Semi ada di mana-mana.
Izinkan saya menjelaskan mengapa saya tidak ingin menggunakan Keamanan Musim Semi. Bagi saya itu terlalu rumit dan entah bagaimana sangat tidak nyaman untuk digunakan di REST. Ya, dan dalam metode pemrosesan titik akhir, Anda mungkin masih harus mengeluarkan pengguna dari konteksnya. Mungkin saya salah, karena saya tidak tahu banyak tentang itu, tapi artikelnya bukan tentang itu.
Saya membutuhkan sesuatu yang sederhana dan mudah digunakan. Ide datang untuk melakukannya melalui anotasi.
Idenya adalah kami menyuntikkan pengguna kami ke setiap metode pengontrol yang memerlukan otorisasi. Dan itu saja. Ternyata di dalam metode pengontrol sudah akan ada pengguna yang diotorisasi dan itu akan menjadi ! = Null (kecuali untuk kasus ketika otorisasi tidak diperlukan).
Kami menemukan alasan untuk membuat motor ini. Sekarang mari kita mulai berlatih.
1. Pembangkitan kunci
Pertama, kita perlu membuat kunci yang akan mengenkripsi informasi minimum yang diperlukan tentang pengguna.
Ada perpustakaan yang sangat nyaman untuk bekerja di java dengan jwt .
Github memiliki semua instruksi tentang cara bekerja dengan jwt, tetapi untuk menyederhanakan prosesnya, saya akan memberikan contoh di bawah ini.
Untuk menghasilkan kunci, buat proyek maven biasa dan tambahkan dependensi berikut
ketergantungan
<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>
Dan kelas yang akan menghasilkan rahasianya
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);
}
}
Hasilnya, kami mendapatkan kunci rahasia, yang akan kami gunakan di masa mendatang.
2. Membuat proyek Musim Semi
Saya tidak akan menjelaskan proses pembuatannya, karena ada banyak artikel dan tutorial tentang topik ini. Dan di situs web resmi Spring ada penginisialisasi , di mana Anda dapat membuat proyek minimal dalam dua klik.
Saya hanya akan meninggalkan file pom terakhir
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>
Setelah membuat proyek, salin kunci yang dibuat sebelumnya ke application.properties
app.api.jwtEncodedSecretKey=teTN1EmB5XADI5iV4daGVAQhBlTwLMAE+LlXZp1JPI2PoQOpgVksRqe79EGOc5opg+AmxOOmyk8q1RbfSWcOyg==
3. TokenHandler
Kami akan membutuhkan layanan untuk menghasilkan dan mendekripsi token.
Token akan berisi minimal informasi tentang pengguna (hanya id-nya) dan waktu kedaluwarsa token. Untuk ini, kami akan membuat antarmuka.
Untuk mentransfer masa pakai token.
Expiration.java
package org.website.jwt;
import java.time.LocalDateTime;
import java.util.Optional;
public interface Expiration {
Optional<LocalDateTime> getAuthTokenExpire();
}
Dan untuk mentransfer ID. Ini akan diimplementasikan oleh entitas pengguna
CreateBy.java
package org.website.jwt;
public interface CreateBy {
Long getId();
}
Kami juga akan membuat implementasi default untuk antarmuka Kedaluwarsa . Secara default, token akan aktif selama 24 jam.
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));
}
}
Mari tambahkan beberapa kelas pembantu.
GeneratedTokenInfo - untuk informasi tentang token yang dibuat.
TokenInfo - untuk informasi tentang token yang datang kepada kami.
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;
}
}
Sekarang TokenHandler itu sendiri . Ini akan menghasilkan token setelah otorisasi pengguna, serta mengambil informasi tentang token yang sebelumnya digunakan oleh pengguna yang sah.
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();
}
}
Saya tidak akan menarik perhatian Anda, karena semuanya harus jelas dengan ini.
4. Anotasi dan penangan
Jadi, setelah semua pekerjaan persiapan, mari beralih ke yang paling menarik. Seperti yang disebutkan sebelumnya, kita membutuhkan anotasi yang akan dimasukkan ke dalam metode pengontrol, di mana diperlukan pengguna yang sah.
Buat anotasi dengan kode berikut
AuthUser.java
package org.website.annotation;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthUser {
boolean required() default true;
}
Dikatakan sebelumnya bahwa otorisasi mungkin bersifat opsional. Hanya untuk ini dan kami membutuhkan metode yang diperlukan dalam ringkasan. Jika otorisasi untuk metode tertentu bersifat opsional dan jika pengguna yang masuk benar-benar tidak diotorisasi, maka null akan dimasukkan ke dalam metode . Tapi kami akan siap untuk ini.
Anotasi telah dibuat, tetapi kita masih membutuhkan penangan , yang akan mendapatkan token dari permintaan, menerimanya dari basis pengguna dan meneruskannya ke metode pengontrol. Spring memiliki antarmuka HandlerMethodArgumentResolver untuk kasus seperti itu . Kami akan menerapkannya.
Buat kelas AuthUserHandlerMethodArgumentResolver yang mengimplementasikan antarmuka di atas.
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));
}
}
Dalam konstruktor, kami menerima nama cookie dan header tempat token dapat diteruskan. Saya mengeluarkannya di application.properties
app.api.tokenKeyName=Auth-Token
app.api.tokenHeaderName=Auth-Token
Sebelumnya diciptakan TokenHandler dan UserJwtSignatureService yang juga lulus dalam constructor .
Kami tidak akan mempertimbangkan UserJwtSignatureService, karena ada ekstraksi standar pengguna dari database dengan id dan tanda tangan tokennya.
Tapi mari kita analisis kode dari pawang itu sendiri secara lebih rinci.
supportParameter - Memeriksa apakah metode memenuhi persyaratan yang diperlukan.
ResolArgument adalah metode utama, di dalamnya semua "keajaiban" terjadi.
Jadi apa yang terjadi disini:
- Kami mendapatkan nilai bidang yang diperlukan dari anotasi kami
- HttpServletRequest
- ,
- required, , .
, , ( , ).
, , , . - , required, , null
Pemroses anotasi telah dibuat. Tapi itu belum semuanya. Itu perlu didaftarkan untuk Spring untuk mengetahuinya. Semuanya sederhana di sini. Buat file konfigurasi yang mengimplementasikan antarmuka WebMvcConfigurer Spring dan ganti metode 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));
}
}
Ini mengakhiri penulisan anotasi.
5. Menangani AuthenticationException
Di bagian sebelumnya, di penangan anotasi, jika otorisasi diperlukan untuk metode pengontrol, tetapi pengguna tidak diberi otorisasi, kami melemparkan AuthenticationException .
Sekarang kita perlu menambahkan kelas dari pengecualian ini dan menanganinya untuk mengembalikan json ke pengguna dengan informasi yang kita butuhkan.
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));
}
}
Dan sekarang pengendali pengecualian itu sendiri. Untuk menangani pengecualian yang muncul dan memberikan pengguna bukan halaman kesalahan Spring standar, tetapi json yang kita butuhkan, Spring memiliki anotasi ControllerAdvice .
Mari tambahkan kelas untuk menangani eksekusi kita.
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();
}
}
Sekarang, jika AuthenticationException dilempar , itu akan ditangkap dan json akan dikembalikan ke pengguna dengan kesalahan AUTHENTICATION_ERROR
6. Pengendali
Sekarang, sebenarnya, untuk itulah semuanya dimulai. Mari buat pengontrol dengan 3 metode:
- Otorisasi wajib
- Dengan tidak ada otorisasi wajib
- Pendaftaran pengguna baru. Kode minimal. Ini hanya menyimpan pengguna ke database, tanpa kata sandi. Yang juga akan mengembalikan token pengguna baru
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();
}
}
Dalam metode required dan notRequired, kami memasukkan anotasi kami.
Dalam kasus pertama, jika pengguna tidak diotorisasi, json harus dikembalikan dengan kesalahan, dan jika diotorisasi, informasi tentang pengguna akan dikembalikan.
Dalam kasus kedua, jika pengguna tidak masuk, pesan Halo tamu! , dan jika diotorisasi, namanya akan dikembalikan.
Mari kita periksa apakah semuanya benar-benar berfungsi.
Pertama, mari kita periksa kedua metode sebagai pengguna yang tidak sah.
Semuanya seperti yang diharapkan. Jika otorisasi diperlukan, kesalahan dikembalikan, dan dalam kasus kedua, pesan Halo tamu! ...
Sekarang mari mendaftar dan mencoba memanggil metode yang sama, tetapi dengan transfer token di header permintaan.
Respons mengembalikan token yang dapat digunakan untuk permintaan tersebut yang memerlukan otorisasi.
Mari kita periksa:
Dalam kasus pertama, hanya informasi tentang pengguna yang dikembalikan. Dalam kasus kedua, pesan selamat datang dikembalikan.
Kerja!
7. Kesimpulan
Metode ini tidak mengklaim sebagai satu-satunya solusi yang benar. Seseorang mungkin lebih suka menggunakan Keamanan Musim Semi. Namun, seperti yang disebutkan di awal, metode ini terbukti, mudah digunakan, dan bekerja dengan sangat baik.




