Интегрируйте токены JWT Token в проекты Spring Security для безопасного доступа к серверному API.
Интегрируйте токены JWT Token в проекты Spring Security для безопасного доступа к серверному API.

введение

Недавно я взялся за частный проект. Бэкэнд был построен с использованием каркаса Spring Boot, а инфраструктура аутентификации и аутентификации использовала Spring Security. В то же время, чтобы гарантировать безопасный доступ клиента к API фоновой службы, токен jwt, содержащий информацию для входа в систему, должен быть возвращен после успешного входа пользователя в систему. Этот токен jwt используется для переноса этого токена jwt. в заголовке запроса в качестве информации аутентификации вызывающего абонента при вызове других интерфейсов. Последний месяц или около того я был занят работой над этим проектом, а с другой стороны, это был замечательный чемпионат мира, поэтому я мало что писал. Часто мне действительно кажется, что написать оригинальную статью гораздо хлопотнее, чем просто набрать код. Однако, если я давно не обновлял статью, мне все равно нужно пересматривать собственную лень. свойственен каждому обычному человеку, желающему добиться успеха и не желающему быть посредственным. Важная задача, которую невозможно решить за всю жизнь.

Введение в JWT

первый,давай компенсируем этоjwtзнание。jwt token Полное имяJSON Web Token , в основном используется для общения в формате JSON между сторонами Объектный режим безопасно передает информацию. Эта информация имеет цифровую подпись, ее можно проверить и ей можно доверять, JWT Можно использовать ключ (с помощью HMAC алгоритм) или использовать RSA или ECDSA пара открытого/закрытого ключей для подписи.

Хотя JWT могут быть зашифрованы для обеспечения конфиденциальности между сторонами, мы сосредоточимся на подписанных токенах. Подписанные токены проверяют целостность содержащихся в них утверждений, а зашифрованные токены скрывают эти утверждения от других сторон. Когда токен подписывается с использованием пары открытого/закрытого ключей, подписать может только сторона, владеющая закрытым ключом.

Применимые сценарии для токена jwt

  • Аутентификация(Authorization):Это самый распространенный сценарий。После входа пользователя в систему,Каждый последующий запрос будет содержать JWT., тем самым позволяя пользователю получать доступ к маршрутам, сервисам и ресурсам, разрешенным токеном. Система единого входа сегодня широко используется. JWT Особенность, поскольку она требует очень небольших накладных расходов и ее можно легко использовать в разных доменах.
  • обмен информацией(Information Exchange):JWTТокенсуществовать Отличный способ безопасной передачи информации между сторонами。因为可к对 JWT Подпишите (например, используя пару открытого/закрытого ключей), чтобы быть уверенным, что отправитель — тот, за кого себя выдает. Кроме того, благодаря использованию headerиpayload Вычисление подписей также проверяет, был ли контент подделан.

Структура jwt

JWT 由headerpayloadиsignatureСостоит из трех частей,к . Расколоть:

header:Обычно по типу токена(即 JWT) и используемый алгоритм подписи (например. HMAC SHA256 или RSA) состоит из двух частей;

Язык кода:javascript
копировать
{
"alg": "HS256",
"typ": "JWT"
}

Затем этот JSON кодируется Base64Url, чтобы сформировать первую часть JWT.

payload: полезная нагрузка。который содержит утверждение,Декларация касается юридического лица(обычно пользователь)и Заявление дополнительных данных。Существует три типа деклараций.:registered, public, private claims.

зарегистрированное заявление:Это набор предопределенных объявлений,не обязательно,Но рекомендуется использовать。к提供一组有用из、Совместимые утверждения. Например: выпуск (эмитент)、истек (срок годности)、предмет、ауд (аудитория) и т. д.

публичное заявление:这些可по причине使用人随意定义。Но чтобы избежать конфликтов,долженсуществоватьjwt token определено при регистрации или определено как содержащее пространство имен для предотвращения коллизий URI。

частная декларация:Это длясуществовать Пользовательские заявления, созданные путем обмена информацией между сторонами, дающими согласие на их использование,Это не заявление о регистрации и не публичное заявление.

Примеры следующие:

Язык кода:javascript
копировать
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Затем полезная нагрузка кодируется Base64Url для формирования второй части веб-токена JSON.

Уведомление,Для подписанных токенов,Эта информация хотя и защищена от несанкционированного доступа.,Но это может прочитать каждый. если не зашифровано,В противном случае не размещайте конфиденциальную информацию в JWT Полезная нагрузка или элемент заголовка.

Signature: Чтобы создать часть подписи, необходимо получить закодированный заголовок, закодированные полезные данные, ключ, алгоритм, указанный в заголовке, и подписать его.

Например, если вы хотите использовать алгоритм HMAC SHA256, подпись будет создана следующим образом:

Язык кода:javascript
копировать
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

Подпись используется для проверки того, что информация не была подделана во время передачи, а в случае токена, подписанного закрытым ключом, она также может проверить, что отправитель JWT является правильным.

полный вес

Строка Base64-URL, разделенная тремя . Ее можно легко передавать в средах HTML и HTTP, и она более компактна, чем стандарты на основе XML, такие как SAML.

Ниже показан JWT, JWT Иметь ранее представленныеheaderиpayloadкодирование,и использоватьключзнак:

Мы можем декодировать, проверить и сгенерировать JWT на веб-сайте https://links.jianshu.com/go?to=https%3A%2F%2Fjwt.io%2F%23debugger-io.

Как использовать JWT

При проверке личности, когда пользователь успешно входит в систему, JSON Web Token。由于Токен凭据,Поэтому необходимо проявлять особую осторожность, чтобы предотвратить проблемы с безопасностью.

Обычно токену необходимо установить срок действия. По истечении срока действия токен становится недействительным и его необходимо заменить новым токеном.

из-за отсутствия безопасности,Конфиденциальные данные сеанса не должны храниться в браузере. Всякий раз, когда пользователю требуется доступ к защищенному ресурсу маршрутизации,Пользовательский агент должен отправить jwt,в целомсуществовать Authorization header используется в Bearer модель. заголовок Содержимое должно выглядеть так:

Язык кода:javascript
копировать
Authorization: Bearer <token>

В некоторых случаях это может быть механизм авторизации без сохранения состояния. Защищенный маршрут сервера будет проверен. Authorization header Есть ли действительный JWT,еслижитьсуществовать,тогда пользователю разрешен доступ к защищенным ресурсам。если JWT Содержит необходимые данные, что может уменьшить необходимость запроса базы данных для определенных операций.,Хотя это не всегда так.

если token существовать Authorization header,Междоменный домен Cross-Origin Resource Sharing (CORS)Не проблема,потому что он не использует cookies

Конкретный процесс получения клиентом токена jwt для доступа к защищенным ресурсам.

1) Пользователь существуетсуществовать. Клиент использует логин/пароль для входа в систему;

2) Сервер использует ключ для генерации токена JWT;

3) Сервер возвращает браузеру сохранившийся токен jwt;

4) Пользователь получает jwt 令牌放到AuthenticationДоступ к защищенным ресурсам на стороне сервера осуществляется в заголовке запроса, соответствующем параметру.иAPI;

5) Сервер проверяет подпись и получает информацию о пользователе из токена jwt;

6) Сервер проверяет подпись и анализирует информацию о пользователе из токена jwt, а затем возвращает клиенту информацию об успешном ответе API.

Использование токена jwt в рамках системы безопасности Spring Security

существовать Нетspring весна под охраной загрузочный проектиспользуется аутентификация по токену vjwt, нам нужно только создать новый перехватчик или фильтр сервлетов. Разобрать jwt Информации о токене достаточно. Если синтаксический анализ успешен, запрос будет выпущен. Если синтаксический анализ завершится неудачно, будет возвращено сообщение 403 Insufficient Permissions. Но существуют Весна Security Сам фреймворк автоматически адаптирует множество фильтров и формирует цепочку фильтров, поэтому нам также необходимо создать новый синтаксический jwt Фильтр токенов необходимо добавить в цепочку фильтров.

Создайте новый проект весенней загрузки.

Используйте IDEA, чтобы создать новый проект весенней загрузки и добавить некоторые необходимые зависимые пакеты jar, такие как Spring MVC, драйвер MySQL, источник данных druid, fast-json и инструмент для краткого кода lombok и т. д.

Язык кода:javascript
копировать
<?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 https://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.2.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bonus</groupId>
    <artifactId>bonus-backend</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>bonus-backend</name>
    <description>bonus-backend</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
       <!--spring web MVC-зависимость -->
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--Измените файлы конфигурации для автоматического вступления в силу и зависимостей.-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!--druid Зависимость источника данных-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.8</version>
        </dependency>
        <!--АлиjsonЗависимости инструментов -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.79</version>
        </dependency>
        <!--mysqlПакет драйверов-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
            <scope>runtime</scope>
        </dependency>
        <!--Краткие инструменты кодаlombokполагаться-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
     <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
</project>

Добавьте Spring Security и зависимости, связанные с JWT

существоватьпроектpom.xmlДокументальный фильмdependenciesДобавить в тег

Язык кода:javascript
копировать
        <!--加解密полагаться-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.11</version>
        </dependency>
        <!--Структура уровня сохраняемостиmybatis-plusполагаться-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.2</version>
        </dependency>
       <!--spring securityполагаться-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.2.7.RELEASE</version>
        </dependency>
        <!--jwt tokenполагаться-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.7.0</version>
        </dependency>

Файл конфигурации проекта

application-porperties

Язык кода:javascript
копировать
server.servlet.context-path=/bonus
spring.profiles.active=dev

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

# mybatis-plus config
mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# twelve.zodiac
twelve.zodiac.mouse=03,15,27,39
twelve.zodiac.cow=02,14,26,38
twelve.zodiac.tiger=01,13,25,37,49
twelve.zodiac.rabbit=12,24,36,48
twelve.zodiac.dragon=11,23,35,47
twelve.zodiac.snake=10,22,34,46
twelve.zodiac.horse=09,21,33,45
twelve.zodiac.sheep=08,20,32,44
twelve.zodiac.monkey=07,19,31,43
twelve.zodiac.chicken=06,18,30,42
twelve.zodiac.dog=05,17,29,41
twelve.zodiac.pig=04,16,28,40

application-dev.properties

Язык кода:javascript
копировать
server.address=127.0.0.1
server.port=8090

spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://localhost:3306/bonus?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.druid.username=bonus_user
spring.datasource.druid.password=tiger2022@
spring.datasource.druid.validation-query=select 1 from dual
#spring.datasource.druid.connect-properties

# redis config
spring.redis.client-name=redis-client
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0

Настройка печати журнала

log4j.properties

Язык кода:javascript
копировать
log4j.rootLogger=DEBUG,stdout
log4j.logger.com.baomidou.mybatisplus=DEBUG
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n

Стартап-класс

Язык кода:javascript
копировать
@SpringBootApplication
@EnableConfigurationProperties
@EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled=true, jsr250Enabled=true)
public class BonusBackendApplication {

    public static void main(String[] args) {
        SpringApplication.run(BonusBackendApplication.class, args);
    }

}

Стартап-классв дополнение к@SpringBootApplicationЗа пределами аннотаций,Также добавляются аннотации для включения свойств конфигурации.@EnableConfigurationPropertiesк及全局安全访问注解@EnableGlobalMethodSecurityВыполнить динамическую проверку разрешений

API, связанный с JWT

Используется для генерации jwt token Хэ Конг jwt tokenАнализ соответствующей информации о пользователеAPIВсесуществоватьcom.auth0.jwt.JWTиcom.auth0.jwt.JWTCreatorв двух категориях。

Методы API в классе JWT

  • public JWT(): Метод конструктора экземпляра класса JWT;
  • public static Builder create(): Создать jwt конструктор токенов, Возвращаемый объектJWTCreatorстатический внутренний класс внутри классаBuilder
  • public DecodedJWT decodeJwt(String token): анализироватьjwt tokenметод
  • public static DecodedJWT decode(String token) : 静态анализироватьjwt метод токена
  • public static Verification require(Algorithm algorithm): Построено с помощью алгоритмовVerification对象静态метод, Класс Verification в основном используется для проверки правильности токена jwt.

Методы API в классе JWTCreator

статический внутренний классBuilderВ основном используется для строительстваheaderиpayload中 содержание, Этот статический класс в основном предоставляет серию методов withXXX для указания соответствующего содержимого пары ключ-значение. В основном он имеет следующие методы API:

  • public JWTCreator.Builder withHeader(Map<String, Object> headerClaims): Создайте коллекцию пар ключ-значение, представленную заголовком;
  • public JWTCreator.Builder withKeyId(String keyId): обозначение令牌headerвkidценить;
  • public JWTCreator.Builder withIssuer(String issuer): Укажите эмитента токена;
  • public JWTCreator.Builder withSubject(String subject): Укажите тему токена;
  • public JWTCreator.Builder withAudience(String... audience): Укажите аудиторию токенов — метод, с помощью которого токены могут быть предоставлены ограниченному числу пользователей;
  • public JWTCreator.Builder withExpiresAt(Date expiresAt): Укажите срок действия токена;
  • public JWTCreator.Builder withNotBefore(Date notBefore): Указывает, что токен нельзя использовать до определенной даты;
  • public JWTCreator.Builder withIssuedAt(Date issuedAt): Укажите дату выпуска токена;
  • public JWTCreator.Builder withJWTId(String jwtId): Укажите идентификатор токена;
  • public JWTCreator.Builder withClaim(String name, Boolean value): обозначениеpayloadв键值对,Значение имеет тип Boolean;
  • public JWTCreator.Builder withClaim(String name, Integer value): обозначениеpayloadв键值对,Значение имеет тип Integer;
  • public JWTCreator.Builder withClaim(String name, Long value) : обозначениеpayloadв键值对,Значение имеет тип Long;
  • public JWTCreator.Builder withClaim(String name, Double value): обозначениеpayloadв键值对,ЗначениеDoubleтип;
  • public JWTCreator.Builder withClaim(String name, String value): обозначениеpayloadв键值对,ЗначениеStringтип;
  • public JWTCreator.Builder withClaim(String name, Date value): обозначениеpayloadв键值对,ЗначениеDateтип;
  • public JWTCreator.Builder withArrayClaim(String name, String[] items): обозначениеpayloadв键值对,ЗначениеString数组тип;
  • public JWTCreator.Builder withArrayClaim(String name, Integer[] items): обозначениеpayloadв键值对,ЗначениеInteger数组тип;
  • public JWTCreator.Builder withArrayClaim(String name, Long[] items): обозначениеpayloadв键值对,ЗначениеLong数组тип;
  • public String sign(Algorithm algorithm) : Метод подписи с помощью подписи алгоритма позволяет получить полный jwt метод содержимого токена

algorithm算法对象可通过静态методAlgorithem#HMAC256или ВОЗAlgorithem#HMAC512метод创建,Введите параметр как одинStringтипизключ

Методы API в классе JWTDecoder

JWTDecoderКласс какDecodedJWTкласс реализации класса,В основном используется для получения от Разобрать. Получите нужную информацию о поле из объекта после токена jwt.

  • public String getAlgorithm(): Получить имя алгоритма подписи;
  • public String getType(): Получите тип токена jwt, по умолчанию — jwt;
  • public String getKeyId(): Получить JWT Значение, соответствующее ребенку в заголовке токена;
  • public Claim getHeaderClaim(String name): Получатьheader中обозначение名字изClaim, Он может дополнительно преобразовывать данные, представленные значением, в различные типы данных;
  • public String getIssuer(): Получить Эмитент токена JWT;
  • public String getSubject():Получить Субъект токена JWT;
  • public List<String> getAudience(): Получить JWT аудитория токена;
  • public Date getExpiresAt(): Получить Срок действия токена JWT;
  • public Date getNotBefore(): Токен нельзя получить ранее момента использования;
  • public String getId(): Получить идентификатор токена;
  • public Claim getClaim(String name): Получатьобозначение名字Claim
  • public Map<String, Claim> getClaims(): Получить JWT令牌вClaimсбор пар ключ-значение;
  • public String getHeader(): Получить JWT令牌вheaderчасть контента;
  • public String getPayload(): Получить JWT令牌вpayloadчасть контента;
  • public String getSignature(): Получить JWT Подписная часть токена;
  • public String getToken(): Восстановить содержимое токена jwt;

Создайте новый класс инструмента токена Jwt.

Используя API, связанный с JWT Мы создали новый класс инструмента JwtTokenUtil. для генерации токен JWT

Язык кода:javascript
копировать
public class JwtTokenUtil {

    // ключ
    private static final String SECRET = "bonusBACKEND2022$";

    // Срок годности 7 дней
    private static final int EXPIRE_SECONDS = 7*24*3600;

    private final static Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);

    /**
     * генерироватьметод токена
     * @param memInfoMap
     * @return jwtToken
     */
    public static String genAuthenticatedToken(Map<String, Object> memInfoMap){
        List<GrantedAuthority> authorities = (List<GrantedAuthority>) memInfoMap.get("authorities");
        String authorityStr = null;
        if(authorities!=null && authorities.size()>0){
            StringBuffer buffer = new StringBuffer();
            for(int i=0; i<authorities.size()-1; i++){
                buffer.append(authorities.get(i).getAuthority()).append(",");
            }
            buffer.append(authorities.get(authorities.size()-1).getAuthority());
            authorityStr = buffer.toString();
        }
        String[] authorityArray = authorityStr!=null?authorityStr.split(","):null;
        Calendar nowTime = Calendar.getInstance();
        //Срок годности
        nowTime.add(Calendar.SECOND, EXPIRE_SECONDS);
        Date expireDate = nowTime.getTime();
        String jwtToken = JWT.create().withJWTId(UUID.randomUUID().toString().replaceAll("-", ""))
                .withClaim("memId", (Long) memInfoMap.get("memId"))
                .withClaim("memAccount", (String) memInfoMap.get("memAccount"))
                .withClaim("memPwd", (String) memInfoMap.get("memPwd"))
                .withClaim("totalCreditAmount", ((BigDecimal) memInfoMap.get("totalCreditAmount")).doubleValue())
                .withClaim("usedCreditAmount", ((BigDecimal) memInfoMap.get("usedCreditAmount")).doubleValue())
                .withClaim("remainCreditAmount", ((BigDecimal) memInfoMap.get("remainCreditAmount")).doubleValue())
                .withArrayClaim("authorities", authorityArray)
                .withIssuedAt(new Date(System.currentTimeMillis()))
                .withExpiresAt(expireDate)
                .sign(Algorithm.HMAC256(SECRET));
        return jwtToken;
    }
}

Реализация методов аутентификации пользователей

UserDetailService#loadUserByUsername

Язык кода:javascript
копировать
@Service
public class MemInfoServiceImpl extends ServiceImpl<MemInfoMapper, MemInfoDTO> implements MemInfoService {
 private final static Logger logger = LoggerFactory.getLogger(MemInfoServiceImpl.class);
    @Resource
    private MyPasswordEncoder passwordEncoder;
    @Resource
    private RoleInfoService roleInfoService;
    
    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MemInfoDTO memInfoDTO = this.baseMapper.getMemInfoByAccount(username);
        if(memInfoDTO==null){
            throw  new UsernameNotFoundException("Username" + username + "is invalid!");
        }
        // Получить список ролей пользователей
        List<RoleInfoDTO> roleInfoDTOList = roleInfoService.getRolesByMemId(memInfoDTO.getMemId());
        if(roleInfoDTOList.size()>0){
            for(RoleInfoDTO roleInfoDTO: roleInfoDTOList){
                SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + roleInfoDTO.getRoleName().toUpperCase());
                memInfoDTO.getAuthorities().add(grantedAuthority);
            }
        }
        return memInfoDTO;
    }

MemInfoDTOИсходный код класса выглядит следующим образом:

Язык кода:javascript
копировать
@Data
@TableName("bonus_mem_info")
@ApiModel(value="MemInfoDTO", description = «Член DTO»)
@Validated
public class MemInfoDTO extends BaseDTO implements UserDetails {

    /**
     * идентификатор участника
     */
    @TableId
    @ApiModelProperty(name = "memId", value = "memId", notes = «Идентификатор участника», dataType = "Long")
    private Long memId;

    /**
     * Учетная запись участника
     */
    @TableField(value = "mem_account")
    @NotEmpty(message = "Учетная запись партияне может быть пустым")
    @ApiModelProperty(name="memAccount", value = "memAccount", notes = "Учетная запись участника", dataType = "String")
    private String memAccount;

    /**
     * Пароль участника
     */
    @TableField(value = "mem_pwd")
    @NotEmpty(message = "Пароль партияне может быть пустым")
    @ApiModelProperty(name="memPwd", value = "memPwd", notes = "Зашифрованный Пароль" участника", dataType = "String")
    private String memPwd;

    /**
     * Тип членства: 1-vip 2-агент;
     */
    @TableField(value = "mem_type")
    @NotEmpty(message = «Тип участника не может быть пустым»)
    @ApiModelProperty(name="memType", value = "memType", notes = «Тип участника», dataType = "Integer", example = "1", allowableValues = "1,2")
    private Integer memType;

    /**
     * Кредитный лимит участника, единица баллов
     */
    @TableField(value = "total_credit_amount")
    @NotEmpty(message = «Кредитный лимит участника не может быть пустым»)
    @ApiModelProperty(name = "totalCreditAmount", value = "totalCreditAmount", notes = «Общий кредитный лимит участника, единица баллов», dataType = "Long", example = "10000")
    private Long totalCreditAmount;

    /**
     * Участник использовал кредитный лимит, единица баллов
     */
    @ApiModelProperty(name = "usedCreditAmount", value = "usedCreditAmount", notes = «Участник использовал кредитный лимит, единица баллов», dataType = "Long", example = "5000")
    @TableField(value = "used_credit_amount")
    private Long usedCreditAmount;

    @TableField(exist = false)
    private List<GrantedAuthority> authorities = new ArrayList<>();

    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return this.memPwd;
    }

    @Override
    public String getUsername() {
        return this.memAccount;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

Создайте новый фильтр аутентификации JwtToken.

Язык кода:javascript
копировать
public class JwtAuthenticationFilterBean extends GenericFilterBean {

    private final static Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilterBean.class);

    private String AUTHORIZATION_NAME = "Authorization";
    
    private String BEARER = "Bearer";
    // Выпустить белый список
    private static List<String> whiteRequestList = new ArrayList<>();
    // Добавьте URL-адрес запроса, не требующий аутентификации и аутентификации, в whiteRequestList.
    static {
        whiteRequestList.add("/bonus/member/checkSafetyCode");
        whiteRequestList.add("/bonus/login");
        whiteRequestList.add("/bonus/member/login");
        whiteRequestList.add("/bonus/common/kaptcha");
        whiteRequestList.add("/bonus/admin/login");
        whiteRequestList.add("/bonus/favicon.ico");
        whiteRequestList.add("/bonus/doc.html");
        whiteRequestList.add("/bonus/error");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        logger.info("requestUrl="+request.getRequestURI());
        //Запросы белого списка и статические ресурсы освобождаются напрямую
        if(whiteRequestList.contains(request.getRequestURI()) || (request.getRequestURI().contains("admin/dist") &&
                request.getRequestURI().endsWith(".css") || request.getRequestURI().equals(".js") ||
                request.getRequestURI().endsWith(".png") || request.getRequestURI().endsWith("favicon.ico"))){
            // если это запрос на проверку логина и кода безопасности, он отправляется напрямую
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        } else {
                String bearerToken = request.getHeader(AUTHORIZATION_NAME);
               if(StringUtils.isEmpty(bearerToken)||!bearerToken.startsWith(BEARER)){
                   printException(response, HttpStatus.UNAUTHORIZED.value(), «Отсутствует токен jwt или ошибка формата токена»);
                   return;
               }
               String authToken = bearerToken.substring(bearerToken.indexOf(BEARER)+BEARER.length()+1);
               if(StringUtils.isEmpty(authToken)){
                   String message = "http header Authorization is null, user Unauthorized";
                   response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                   response.setStatus(HttpStatus.UNAUTHORIZED.value());
                   this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
                   return;
               }  else {
                   try {
                       // Разобрать токен jwt, Если это режим Bearer, то нужно сначала определить, является ли префикс Bearer, а затем перехватить его.
                       // Содержимое после пространства Bearer будет проанализировано снова.
                       DecodedJWT decodedJWT = JWT.decode(authToken);
                       Map<String, Claim> claimMap = decodedJWT.getClaims();
                       Claim expireClaim = claimMap.get("exp");
                       Date expireDate = expireClaim.asDate();
                       // Токен подтверждения Срок годности истек?
                       if(expireDate.before(DateUtil.date(System.currentTimeMillis()))){
                           String message = "Authorization token expired";
                           this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
                           return;
                       }
                       Claim memAccountClaim = claimMap.get("memAccount");
                       if(memAccountClaim==null || StringUtils.isEmpty(memAccountClaim.asString())){
                           String message = "memAccount cannot be null";
                           response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                           response.setStatus(HttpStatus.UNAUTHORIZED.value());
                           this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
                           return;
                       }
                       // Аутентификация заголовка запроса пройдена, Запрос на выпуск
                       filterChain.doFilter(servletRequest, servletResponse);
                   } catch (JWTDecodeException e) {
                       String message = "JWT decode authToken failed, caused by " + e.getMessage();
                       this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
                       return;
                   }
               }
        }

    }

    /**
     * Распечатать информацию об ошибке аутентификации заголовка запроса
     * @param response
     * @param status
     * @param message
     * @throws IOException
     */
    private void printException(HttpServletResponse response, int status, String message) throws IOException {
        logger.error(message);
        response.setStatus(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        PrintWriter printWriter = response.getWriter();
        ResponseResult<String> responseResult = ResponseResult.error(status, message);
        printWriter.write(JSONObject.toJSONString(responseResult));
        printWriter.flush();
        printWriter.close();
    }
}

Класс конфигурации Spring Security настроен на возврат токена jwt после успешного входа в систему.

Язык кода:javascript
копировать
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final static Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

    @Resource
    private MemInfoService memInfoService;

    private MathContext mathContext = new MathContext(2, RoundingMode.HALF_UP);

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
        auth.userDetailsService(memInfoService);
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/static/**","/index.html","/templates/**", "/admin/**", "/doc.html", "/webjars/**", "/v2/*", "/favicon.ico", "/swagger-resources");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        JwtAuthenticationFilterBean jwtAuthenticationFilterBean = new JwtAuthenticationFilterBean();
        http.addFilterBefore(jwtAuthenticationFilterBean, UsernamePasswordAuthenticationFilter.class); // Зарегистрируйте фильтр аутентификации JwtToken, который существует перед входом в фильтр аутентификации.
        // Настройка междоменного доступа
        http.cors().configurationSource(corsConfigurationSource())
                .and().logout().invalidateHttpSession(true).logoutUrl("/member/logout").permitAll()
        ;
        http.authorizeRequests().antMatchers("/member/checkSafetyCode").permitAll()
                .antMatchers("/doc.html").permitAll()
                .antMatchers("/common/kaptcha").permitAll()
                .antMatchers("/admin/login").permitAll()
                .anyRequest().authenticated()
                .and().httpBasic()
                .and().formLogin()
                .loginProcessingUrl("/member/login") // Интерфейс входа в систему
                .successHandler((httpServletRequest, httpServletResponse, authentication) -> {
                     httpServletResponse.setContentType("application/json;charset=utf-8");
                     httpServletResponse.setStatus(HttpStatus.OK.value());
                     PrintWriter printWriter = httpServletResponse.getWriter();
                     MemInfoDTO memInfoDTO = (MemInfoDTO) authentication.getPrincipal();
                     Map<String, Object> userMap = new HashMap<>();
                     userMap.put("memId", memInfoDTO.getMemId());
                     userMap.put("memAccount", memInfoDTO.getMemAccount());
                     userMap.put("memPwd", memInfoDTO.getMemPwd());
                     BigDecimal totalCredit = memInfoDTO.getTotalCreditAmount()!=null?new BigDecimal(memInfoDTO.getTotalCreditAmount()/100, mathContext): new BigDecimal("0.0");
                     userMap.put("totalCreditAmount", totalCredit);
                     BigDecimal usedCredit = memInfoDTO.getUsedCreditAmount()!=null?new BigDecimal(memInfoDTO.getUsedCreditAmount()/100, mathContext):new BigDecimal("0.0");
                     userMap.put("usedCreditAmount", usedCredit);
                     Long remainCredit = (memInfoDTO.getTotalCreditAmount()==null?0:memInfoDTO.getTotalCreditAmount()) - (memInfoDTO.getUsedCreditAmount()==null?0:memInfoDTO.getUsedCreditAmount());
                     BigDecimal remainCreditAmount = new BigDecimal(remainCredit/100, mathContext);
                     userMap.put("remainCreditAmount", remainCreditAmount);
                     userMap.put("authorities", memInfoDTO.getAuthorities());
                     Map<String, Object> dataMap = new HashMap<>();
                     dataMap.put("memInfo", userMap);
                     dataMap.put("authenticatedToken", JwtTokenUtil.genAuthenticatedToken(userMap));
                     ResponseResult<Map<String, Object>> responseResult = ResponseResult.success(dataMap, "login success");
                     printWriter.write(JSONObject.toJSONString(responseResult));
                     printWriter.flush();
                     printWriter.close();
                }).failureHandler((httpServletRequest, httpServletResponse, e) -> {
                     logger.error("login failed, caused by " + e.getMessage());
                     httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
                     httpServletResponse.setStatus(HttpStatus.OK.value());
                     PrintWriter printWriter = httpServletResponse.getWriter();
                     ResponseResult<String> responseResult = ResponseResult.error(HttpStatus.UNAUTHORIZED.value(), "authentication failed");
                     responseResult.setPath(httpServletRequest.getRequestURI());
                     printWriter.write(JSONObject.toJSONString(responseResult));
                     printWriter.flush();
                     printWriter.close();
                }).permitAll()
                .and().csrf().disable().exceptionHandling().accessDeniedHandler(accessDeniedHandler());

    }

    //Настройка междоменного доступ Доступ к ресурсам
    private CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source =   new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");	//Одинаковая конфигурация источника, * означает, что любой запрос считается от одного и того же источника. Если вам нужно указать IP и порт, вы можете изменить его на «localhost:8080» и разделить несколько запросов с помощью «,»;
        corsConfiguration.addAllowedHeader("*");//заголовок, какие заголовки разрешены, в данном случае используется в — токен, здесь можно заменить * на токен;
        corsConfiguration.addAllowedMethod("*");	//Разрешенные методы запроса, PSOT, GET и т. д.
        corsConfiguration.setAllowCredentials(true);
        // Регистрация междоменной конфигурации
        source.registerCorsConfiguration("/**",corsConfiguration); //Настройте URL-адреса, разрешающие междоменный доступ
        return source;
    }

    @Bean
    AccessDeniedHandler accessDeniedHandler() {
        return new AuthenticationAccessDeniedHandler();
    }
}

Тестовый эффект

После запуска метода Main в существующем Стартап-классе вы можете Тестовый эффект

Тест для создания токена jwt

наспервый测试генерироватьjwt tokenиз Интерфейс входа в систему, существоватьpostmanпозвонить Интерфейс входа в систему

post http://localhost:8090/bonus/member/login??username=zhangsan&password=zhangsan1234

Интерфейс возвращает следующую информацию:

Язык кода:javascript
копировать
{
    "code": 200,
    "data": {
        "memInfo": {
            "memAccount": "zhangsan",
            "totalCreditAmount": 2000,
            "memPwd": "82dea760d7bb362ca74883836ee4d6ba",
            "remainCreditAmount": 2000,
            "usedCreditAmount": 0,
            "authorities": [
                {
                    "authority": "ROLE_USER"
                }
            ],
            "memId": 1592927262097924097
        },
        "authenticatedToken": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZW1BY2NvdW50IjoiemhhbmdzYW4iLCJ0b3RhbENyZWRpdEFtb3VudCI6MjAwMC4wLCJtZW1Qd2QiOiI4MmRlYTc2MGQ3YmIzNjJjYTc0ODgzODM2ZWU0ZDZiYSIsInJlbWFpbkNyZWRpdEFtb3VudCI6MjAwMC4wLCJ1c2VkQ3JlZGl0QW1vdW50IjowLjAsImV4cCI6MTY3MjU1ODAyMSwiaWF0IjoxNjcxOTUzMjIxLCJqdGkiOiI2M2M1YmExZDIzZGY0YjIzODQ1NWU5YjkwNzQzMzRmMSIsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJtZW1JZCI6MTU5MjkyNzI2MjA5NzkyNDA5N30.S5UQLasL-SALKBHwhhUk_DGv__YPlRJQ7TC1pBzxb0g"
    },
    "message": "login success"
}

memPwd字段为密码加密后из密文

authenticatedToken 对应из内容为Bearer模式изтокен jwt, Настоящее содержимое токена jwt — это более длинная строка, начинающаяся с eyj.

Тест прошел аутентификацию и аутентификацию токена jwt

Создайте новый интерфейс для получения данных конфигурации.

Язык кода:javascript
копировать
@RestController
@RequestMapping("/config")
public class ConfigController {

    @Resource
    private ZodiacProperties zodiacProperties;

    @GetMapping("/twelve/zodiacs")
    public ResponseResult<ZodiacProperties> getTwelveZodiacs(){

        return ResponseResult.success(zodiacProperties);
    }

}

ZodiacPropertiesИсходный код класса выглядит следующим образом:

Язык кода:javascript
копировать
@Component
@ConfigurationProperties(prefix = "twelve.zodiac")
public class ZodiacProperties {

    private String mouse;

    private String cow;

    private String tiger;

    private String rabbit;

    private String dragon;

    private String snake;

    private String horse;

    private String sheep;

    private String monkey;

    private String chicken;

    private String dog;

    private String pig;
    // Опустить методы set и get
}

После написания интерфейса перезапустите фоновую службу и снова войдите в систему, чтобы получить токен jwt.

первый Попробуйте или нетсуществовать Добавить в заголовок запросатокен JWTиз结果

GET http://localhost:8090/bonus/config/twelve/zodiacs

Интерфейс возвращает результаты:

Язык кода:javascript
копировать
{
    "code": 401,
    "message": «Отсутствует токен jwt или ошибка формата токена»
}

Затемсуществовать Добавить в заголовок запросаAuthenticationпараметртокен JWTРезультаты теста еще раз:

В это время возвращается результат:

Язык кода:javascript
копировать
{
    "code": 200,
    "message": "ok",
    "path": null,
    "data": {
        "mouse": "03,15,27,39",
        "cow": "02,14,26,38",
        "tiger": "01,13,25,37,49",
        "rabbit": "12,24,36,48",
        "dragon": "11,23,35,47",
        "snake": "10,22,34,46",
        "horse": "09,21,33,45",
        "sheep": "08,20,32,44",
        "monkey": "07,19,31,43",
        "chicken": "06,18,30,42",
        "dog": "05,17,29,41",
        "pig": "04,16,28,40"
    }
}

о том, каксуществоватьинтегрированныйspring security安全访问框架изspring bootКак использовать в проектетокен JWTБезопасный доступ к серверуAPIВот и все

Справочное чтение

【1】Введение в токен JWT (https://www.jianshu.com/p/fa957f32806a)

boy illustration
Неразрушающее увеличение изображений одним щелчком мыши, чтобы сделать их более четкими артефактами искусственного интеллекта, включая руководства по установке и использованию.
boy illustration
Копикодер: этот инструмент отлично работает с Cursor, Bolt и V0! Предоставьте более качественные подсказки для разработки интерфейса (создание навигационного веб-сайта с использованием искусственного интеллекта).
boy illustration
Новый бесплатный RooCline превосходит Cline v3.1? ! Быстрее, умнее и лучше вилка Cline! (Независимое программирование AI, порог 0)
boy illustration
Разработав более 10 проектов с помощью Cursor, я собрал 10 примеров и 60 подсказок.
boy illustration
Я потратил 72 часа на изучение курсорных агентов, и вот неоспоримые факты, которыми я должен поделиться!
boy illustration
Идеальная интеграция Cursor и DeepSeek API
boy illustration
DeepSeek V3 снижает затраты на обучение больших моделей
boy illustration
Артефакт, увеличивающий количество очков: на основе улучшения характеристик препятствия малым целям Yolov8 (SEAM, MultiSEAM).
boy illustration
DeepSeek V3 раскручивался уже три дня. Сегодня я попробовал самопровозглашенную модель «ChatGPT».
boy illustration
Open Devin — инженер-программист искусственного интеллекта с открытым исходным кодом, который меньше программирует и больше создает.
boy illustration
Эксклюзивное оригинальное улучшение YOLOv8: собственная разработка SPPF | SPPF сочетается с воспринимаемой большой сверткой ядра UniRepLK, а свертка с большим ядром + без расширения улучшает восприимчивое поле
boy illustration
Популярное и подробное объяснение DeepSeek-V3: от его появления до преимуществ и сравнения с GPT-4o.
boy illustration
9 основных словесных инструкций по доработке академических работ с помощью ChatGPT, эффективных и практичных, которые стоит собрать
boy illustration
Вызовите deepseek в vscode для реализации программирования с помощью искусственного интеллекта.
boy illustration
Познакомьтесь с принципами сверточных нейронных сетей (CNN) в одной статье (суперподробно)
boy illustration
50,3 тыс. звезд! Immich: автономное решение для резервного копирования фотографий и видео, которое экономит деньги и избавляет от беспокойства.
boy illustration
Cloud Native|Практика: установка Dashbaord для K8s, графика неплохая
boy illustration
Краткий обзор статьи — использование синтетических данных при обучении больших моделей и оптимизации производительности
boy illustration
MiniPerplx: новая поисковая система искусственного интеллекта с открытым исходным кодом, спонсируемая xAI и Vercel.
boy illustration
Конструкция сервиса Synology Drive сочетает проникновение в интрасеть и синхронизацию папок заметок Obsidian в облаке.
boy illustration
Центр конфигурации————Накос
boy illustration
Начинаем с нуля при разработке в облаке Copilot: начать разработку с минимальным использованием кода стало проще
boy illustration
[Серия Docker] Docker создает мультиплатформенные образы: практика архитектуры Arm64
boy illustration
Обновление новых возможностей coze | Я использовал coze для создания апплета помощника по исправлению домашних заданий по математике
boy illustration
Советы по развертыванию Nginx: практическое создание статических веб-сайтов на облачных серверах
boy illustration
Feiniu fnos использует Docker для развертывания личного блокнота Notepad
boy illustration
Сверточная нейронная сеть VGG реализует классификацию изображений Cifar10 — практический опыт Pytorch
boy illustration
Начало работы с EdgeonePages — новым недорогим решением для хостинга веб-сайтов
boy illustration
[Зона легкого облачного игрового сервера] Управление игровыми архивами
boy illustration
Развертывание SpringCloud-проекта на базе Docker и Docker-Compose