Недавно я взялся за частный проект. Бэкэнд был построен с использованием каркаса Spring Boot, а инфраструктура аутентификации и аутентификации использовала Spring Security. В то же время, чтобы гарантировать безопасный доступ клиента к API фоновой службы, токен jwt, содержащий информацию для входа в систему, должен быть возвращен после успешного входа пользователя в систему. Этот токен jwt используется для переноса этого токена jwt. в заголовке запроса в качестве информации аутентификации вызывающего абонента при вызове других интерфейсов. Последний месяц или около того я был занят работой над этим проектом, а с другой стороны, это был замечательный чемпионат мира, поэтому я мало что писал. Часто мне действительно кажется, что написать оригинальную статью гораздо хлопотнее, чем просто набрать код. Однако, если я давно не обновлял статью, мне все равно нужно пересматривать собственную лень. свойственен каждому обычному человеку, желающему добиться успеха и не желающему быть посредственным. Важная задача, которую невозможно решить за всю жизнь.
первый,давай компенсируем этоjwtзнание。jwt token
Полное имяJSON Web Token
, в основном используется для общения в формате JSON между сторонами Объектный режим безопасно передает информацию. Эта информация имеет цифровую подпись, ее можно проверить и ей можно доверять, JWT Можно использовать ключ (с помощью HMAC
алгоритм) или использовать RSA
или ECDSA
пара открытого/закрытого ключей для подписи.
Хотя JWT могут быть зашифрованы для обеспечения конфиденциальности между сторонами, мы сосредоточимся на подписанных токенах. Подписанные токены проверяют целостность содержащихся в них утверждений, а зашифрованные токены скрывают эти утверждения от других сторон. Когда токен подписывается с использованием пары открытого/закрытого ключей, подписать может только сторона, владеющая закрытым ключом.
header
иpayload
Вычисление подписей также проверяет, был ли контент подделан.JWT 由header
、payload
иsignature
Состоит из трех частей,к . Расколоть:
header:Обычно по типу токена(即 JWT) и используемый алгоритм подписи (например. HMAC SHA256 или RSA) состоит из двух частей;
{
"alg": "HS256",
"typ": "JWT"
}
Затем этот JSON кодируется Base64Url, чтобы сформировать первую часть JWT.
payload: полезная нагрузка。который содержит утверждение,Декларация касается юридического лица(обычно пользователь)и Заявление дополнительных данных。Существует три типа деклараций.:registered, public, private claims
.
зарегистрированное заявление:Это набор предопределенных объявлений,не обязательно,Но рекомендуется использовать。к提供一组有用из、Совместимые утверждения. Например: выпуск (эмитент)、истек (срок годности)、предмет、ауд (аудитория) и т. д.
публичное заявление:这些可по причине使用人随意定义。Но чтобы избежать конфликтов,долженсуществоватьjwt token определено при регистрации или определено как содержащее пространство имен для предотвращения коллизий URI。
частная декларация:Это длясуществовать Пользовательские заявления, созданные путем обмена информацией между сторонами, дающими согласие на их использование,Это не заявление о регистрации и не публичное заявление.
Примеры следующие:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Затем полезная нагрузка кодируется Base64Url для формирования второй части веб-токена JSON.
Уведомление,Для подписанных токенов,Эта информация хотя и защищена от несанкционированного доступа.,Но это может прочитать каждый. если не зашифровано,В противном случае не размещайте конфиденциальную информацию в JWT Полезная нагрузка или элемент заголовка.
Signature: Чтобы создать часть подписи, необходимо получить закодированный заголовок, закодированные полезные данные, ключ, алгоритм, указанный в заголовке, и подписать его.
Например, если вы хотите использовать алгоритм HMAC SHA256, подпись будет создана следующим образом:
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.
При проверке личности, когда пользователь успешно входит в систему, JSON Web Token
。由于Токен凭据,Поэтому необходимо проявлять особую осторожность, чтобы предотвратить проблемы с безопасностью.
Обычно токену необходимо установить срок действия. По истечении срока действия токен становится недействительным и его необходимо заменить новым токеном.
из-за отсутствия безопасности,Конфиденциальные данные сеанса не должны храниться в браузере. Всякий раз, когда пользователю требуется доступ к защищенному ресурсу маршрутизации,Пользовательский агент должен отправить jwt,в целомсуществовать Authorization header используется в Bearer модель. заголовок Содержимое должно выглядеть так:
Authorization: Bearer <token>
В некоторых случаях это может быть механизм авторизации без сохранения состояния. Защищенный маршрут сервера будет проверен. Authorization header Есть ли действительный JWT,еслижитьсуществовать,тогда пользователю разрешен доступ к защищенным ресурсам。если JWT Содержит необходимые данные, что может уменьшить необходимость запроса базы данных для определенных операций.,Хотя это не всегда так.
если token
существовать Authorization header
,Междоменный домен Cross-Origin Resource Sharing (CORS)
Не проблема,потому что он не использует cookies
。
1) Пользователь существуетсуществовать. Клиент использует логин/пароль для входа в систему;
2) Сервер использует ключ для генерации токена JWT;
3) Сервер возвращает браузеру сохранившийся токен jwt;
4) Пользователь получает jwt 令牌放到Authentication
Доступ к защищенным ресурсам на стороне сервера осуществляется в заголовке запроса, соответствующем параметру.иAPI;
5) Сервер проверяет подпись и получает информацию о пользователе из токена jwt;
6) Сервер проверяет подпись и анализирует информацию о пользователе из токена jwt, а затем возвращает клиенту информацию об успешном ответе API.
существовать Нетspring весна под охраной загрузочный проектиспользуется аутентификация по токену vjwt, нам нужно только создать новый перехватчик или фильтр сервлетов. Разобрать jwt Информации о токене достаточно. Если синтаксический анализ успешен, запрос будет выпущен. Если синтаксический анализ завершится неудачно, будет возвращено сообщение 403 Insufficient Permissions. Но существуют Весна Security Сам фреймворк автоматически адаптирует множество фильтров и формирует цепочку фильтров, поэтому нам также необходимо создать новый синтаксический jwt Фильтр токенов необходимо добавить в цепочку фильтров.
Используйте IDEA, чтобы создать новый проект весенней загрузки и добавить некоторые необходимые зависимые пакеты jar, такие как Spring MVC, драйвер MySQL, источник данных druid, fast-json и инструмент для краткого кода lombok и т. д.
<?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>
существоватьпроектpom.xml
Документальный фильмdependencies
Добавить в тег
<!--加解密полагаться-->
<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
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
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
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
@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
Выполнить динамическую проверку разрешений
Используется для генерации 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;Используя API, связанный с JWT Мы создали новый класс инструмента JwtTokenUtil. для генерации токен JWT
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
@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
Исходный код класса выглядит следующим образом:
@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;
}
}
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();
}
}
@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 token
из Интерфейс входа в систему, существоватьpostmanпозвонить Интерфейс входа в систему
post http://localhost:8090/bonus/member/login??username=zhangsan&password=zhangsan1234
Интерфейс возвращает следующую информацию:
{
"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.
Создайте новый интерфейс для получения данных конфигурации.
@RestController
@RequestMapping("/config")
public class ConfigController {
@Resource
private ZodiacProperties zodiacProperties;
@GetMapping("/twelve/zodiacs")
public ResponseResult<ZodiacProperties> getTwelveZodiacs(){
return ResponseResult.success(zodiacProperties);
}
}
ZodiacProperties
Исходный код класса выглядит следующим образом:
@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
Интерфейс возвращает результаты:
{
"code": 401,
"message": «Отсутствует токен jwt или ошибка формата токена»
}
Затемсуществовать Добавить в заголовок запросаAuthenticationпараметртокен JWTРезультаты теста еще раз:
В это время возвращается результат:
{
"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)