Spring Boot 2 + Spring Security 5 + Одностраничное приложение JWT Restful Solution

Spring Boot

Я уже писал подобное руководство раньше, но меня не удовлетворил спекулятивный подход, не учитывавший официальный дизайн Spring Security. Этот период времени относительно пуст, поэтому я буду изучать его снова.

Оригинальный адрес:woohoo.in lighting.org/archives/tickets…

Гитхаб проекта:GitHub.com/Smith-C Швейцария…

старая версия:GitHub.com/Smith-C Швейцария…

характеристика

  • Используйте JWT для аутентификации, истечение срока действия токена поддержки
  • Используйте Ehcache для кэширования, чтобы уменьшить нагрузку на базу данных для каждой аутентификации.
  • Максимально приближен к дизайну Spring Security
  • Реализовать контроль разрешений на аннотации

Подготовить

Приступая к этому руководству, я надеюсь иметь общее представление о следующих пунктах знаний.

  • Знание основных концепций JWT
  • Узнал о Spring Security

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

Shiro+JWT+Spring Boot Restful Простое руководство

Spring Boot + Spring Security + Простой учебник Thymeleaf

в этом проектеJWTКлючевым моментом является использование собственного пароля пользователя для входа, чтобы каждыйtokenКлючи разные и относительно безопасные.

Главная идея:

авторизоваться:

  1. Имя пользователя и пароль POST для \login
  2. запрос поступаетJwtAuthenticationFilterсерединаattemptAuthentication()метод, получить параметры POST в запросе и обернуть его вUsernamePasswordAuthenticationTokenДоставлен вAuthenticationManagerизauthenticate()метод аутентификации.
  3. AuthenticationManagerбудет отCachingUserDetailsServiceНайдите информацию о пользователе в и определите, правильный ли пароль учетной записи.
  4. Если пароль учетной записи правильный, перейдите кJwtAuthenticationFilterсерединаsuccessfulAuthentication()мы подписываем, генерируем токен и возвращаем его пользователю.
  5. Если пароль учетной записи неверный, перейдите кJwtAuthenticationFilterсерединаunsuccessfulAuthentication()метод, мы возвращаем сообщение об ошибке, чтобы пользователь мог снова войти в систему.

Запрос аутентификации:

Основная идея запроса аутентификации заключается в том, что мы получим токен из поля Authorization в запросе, если пользователя в этом поле нет, Spring Security будет использовать его по умолчанию.AnonymousAuthenticationToken()Обернуть его, т.е. представить анонимных пользователей.

  1. Инициировать любой запрос
  2. прибытьJwtAuthorizationFilterсерединаdoFilterInternal()метод аутентификации.
  3. Если аутентификация прошла успешно, мы сгенерируемAuthenticationиспользоватьSecurityContextHolder.getContext().setAuthentication()Помещение его в Безопасность означает, что аутентификация завершена. Как аутентифицироваться здесь написано нашим собственным кодом, который будет подробно объяснен позже.

подготовить 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.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.inlighting</groupId>
    <artifactId>spring-boot-security-jwt</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-security-jwt</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- JWT 支持 -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.8.2</version>
        </dependency>

        <!-- cache 支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

        <!-- cache 支持 -->
        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
        </dependency>

        <!-- cache 支持 -->
        <dependency>
            <groupId>javax.cache</groupId>
            <artifactId>cache-api</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- ehcache 读取 xml 配置文件使用 -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!-- ehcache 读取 xml 配置文件使用 -->
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!-- ehcache 读取 xml 配置文件使用 -->
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!-- ehcache 读取 xml 配置文件使用 -->
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

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

<!-- ehcache 读取 xml 配置文件使用 -->
<dependency>
  <groupId>javax.xml.bind</groupId>
  <artifactId>jaxb-api</artifactId>
  <version>2.3.0</version>
</dependency>

<!-- ehcache 读取 xml 配置文件使用 -->
<dependency>
  <groupId>com.sun.xml.bind</groupId>
  <artifactId>jaxb-impl</artifactId>
  <version>2.3.0</version>
</dependency>

<!-- ehcache 读取 xml 配置文件使用 -->
<dependency>
  <groupId>com.sun.xml.bind</groupId>
  <artifactId>jaxb-core</artifactId>
  <version>2.3.0</version>
</dependency>

<!-- ehcache 读取 xml 配置文件使用 -->
<dependency>
  <groupId>javax.activation</groupId>
  <artifactId>activation</artifactId>
  <version>1.1.1</version>
</dependency>

Поскольку ehcache использует эти зависимости при чтении файлов конфигурации xml, а эти зависимости являются необязательными модулями, начиная с JDK 9, поэтому пользователям более высоких версий необходимо добавить эти зависимости, чтобы использовать их в обычном режиме.

Основная подготовка к работе

Затем подготовьтесь к следующей основной работе, которая заключается в создании нового объекта, моделировании базы данных и написании класса инструментов JWT для основных операций.

UserEntity.java

Объясните, почему роль использует GrantedAuthority: На самом деле, для упрощения кода используется непосредственно готовый ролевой класс Security, в реальных проектах мы должны сами его обработать и преобразовать в ролевой класс Security.

public class UserEntity {

    public UserEntity(String username, String password, Collection<? extends GrantedAuthority> role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }

    private String username;

    private String password;

    private Collection<? extends GrantedAuthority> role;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Collection<? extends GrantedAuthority> getRole() {
        return role;
    }

    public void setRole(Collection<? extends GrantedAuthority> role) {
        this.role = role;
    }
}

ResponseEntity.java

Разделение внешнего и внутреннего интерфейса Чтобы упростить внешний интерфейс, нам нужно унифицировать формат возврата json, поэтому мы настраиваем ResponseEntity.java.

public class ResponseEntity {

    public ResponseEntity() {
    }

    public ResponseEntity(int status, String msg, Object data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }

    private int status;

    private String msg;

    private Object data;

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

Database.java

Здесь мы используем HashMap для моделирования базы данных, пароль, который я использовал заранееBcryptЗашифрованный, это также алгоритм шифрования, официально рекомендованный Spring Security (шифрование MD5 было удалено в Spring Security 5, это небезопасно).

имя пользователя пароль разрешение
jack jack123 сохранить Bcrypt в зашифрованном виде ROLE_USER
danny danny123 сохраняет Bcrypt в зашифрованном виде ROLE_EDITOR
smith smith123 сохраняет Bcrypt в зашифрованном виде ROLE_ADMIN
@Component
public class Database {
    private Map<String, UserEntity> data = null;
    
    public Map<String, UserEntity> getDatabase() {
        if (data == null) {
            data = new HashMap<>();

            UserEntity jack = new UserEntity(
                    "jack",
                    "$2a$10$AQol1A.LkxoJ5dEzS5o5E.QG9jD.hncoeCGdVaMQZaiYZ98V/JyRq",
                    getGrants("ROLE_USER"));
            UserEntity danny = new UserEntity(
                    "danny",
                    "$2a$10$8nMJR6r7lvh9H2INtM2vtuA156dHTcQUyU.2Q2OK/7LwMd/I.HM12",
                    getGrants("ROLE_EDITOR"));
            UserEntity smith = new UserEntity(
                    "smith",
                    "$2a$10$E86mKigOx1NeIr7D6CJM3OQnWdaPXOjWe4OoRqDqFgNgowvJW9nAi",
                    getGrants("ROLE_ADMIN"));
            data.put("jack", jack);
            data.put("danny", danny);
            data.put("smith", smith);
        }
        return data;
    }
    
    private Collection<GrantedAuthority> getGrants(String role) {
        return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
    }
}

UserService.java

Здесь мы моделируем другой сервис, в основном для имитации работы базы данных.

@Service
public class UserService {

    @Autowired
    private Database database;

    public UserEntity getUserByUsername(String username) {
        return database.getDatabase().get(username);
    }
}

JwtUtil.java

Класс инструментов, написанный мной, в основном отвечающий за подпись и аутентификацию JWT.

public class JwtUtil {

    // 过期时间5分钟
    private final static long EXPIRE_TIME = 5 * 60 * 1000;

    /**
     * 生成签名,5min后过期
     * @param username 用户名
     * @param secret 用户的密码
     * @return 加密的token
     */
    public static String sign(String username, String secret) {
        Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(expireDate)
                    .sign(algorithm);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 校验token是否正确
     * @param token 密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}

Весенняя модернизация безопасности

Вход в этот блок, мы используем пользовательскийJwtAuthenticationFilterчтобы залогиниться.

Запрос аутентификации, мы используем пользовательскийJwtAuthorizationFilterобрабатывать.

Может быть, все думают, что эти два слова немного похожи по длине, 😜.

UserDetailsServiceImpl.java

Сначала реализуем официальныйUserDetailsServiceИнтерфейс здесь в основном отвечает за операцию получения данных из базы данных.

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userService.getUserByUsername(username);
        if (userEntity == null) {
            throw new UsernameNotFoundException("This username didn't exist.");
        }
        return new User(userEntity.getUsername(), userEntity.getPassword(), userEntity.getRole());
    }
}

В пост-последовательности нам также необходимо ее кэшировать и преобразовывать, иначе каждый запрос должен будет получать аутентификацию данных из базы данных, что будет слишком сильно нагружать базу данных.

JwtAuthenticationFilter.java

Этот фильтр в основном обрабатывает операцию входа в систему, которую мы унаследовалиUsernamePasswordAuthenticationFilter, что значительно упрощает нашу работу.

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    /*
    过滤器一定要设置 AuthenticationManager,所以此处我们这么编写,这里的 AuthenticationManager
    我会从 Security 配置的时候传入
    */
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        /*
        运行父类 UsernamePasswordAuthenticationFilter 的构造方法,能够设置此滤器指定
        方法为 POST [\login]
        */
        super();
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 从请求的 POST 中拿取 username 和 password 两个字段进行登入
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        // 设置一些客户 IP 啥信息,后面想用的话可以用,虽然没啥用
        setDetails(request, token);
        // 交给 AuthenticationManager 进行鉴权
        return getAuthenticationManager().authenticate(token);
    }

    /*
    鉴权成功进行的操作,我们这里设置返回加密后的 token
    */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        handleResponse(request, response, authResult, null);
    }

    /*
    鉴权失败进行的操作,我们这里就返回 用户名或密码错误 的信息
    */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        handleResponse(request, response, null, failed);
    }

    private void handleResponse(HttpServletRequest request, HttpServletResponse response, Authentication authResult, AuthenticationException failed) throws IOException, ServletException {
        ObjectMapper mapper = new ObjectMapper();
        ResponseEntity responseEntity = new ResponseEntity();
        response.setHeader("Content-Type", "application/json;charset=UTF-8");
        if (authResult != null) {
            // 处理登入成功请求
            User user = (User) authResult.getPrincipal();
            String token = JwtUtil.sign(user.getUsername(), user.getPassword());
            responseEntity.setStatus(HttpStatus.OK.value());
            responseEntity.setMsg("登入成功");
            responseEntity.setData("Bearer " + token);
            response.setStatus(HttpStatus.OK.value());
            response.getWriter().write(mapper.writeValueAsString(responseEntity));
        } else {
            // 处理登入失败请求
            responseEntity.setStatus(HttpStatus.BAD_REQUEST.value());
            responseEntity.setMsg("用户名或密码错误");
            responseEntity.setData(null);
            response.setStatus(HttpStatus.BAD_REQUEST.value());
            response.getWriter().write(mapper.writeValueAsString(responseEntity));
        }
    }
}

private void handleResponse()Метод обработки здесь не очень хорош Моя идея состоит в том, чтобы перейти к контроллеру для обработки, но токен, который был успешно аутентифицирован, не может быть перенесен, поэтому я написал его первым, что немного сложно.

JwtAuthorizationFilter.java

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

public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    private UserDetailsService userDetailsService;

    // 会从 Spring Security 配置文件那里传过来
    public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) {
        super(authenticationManager);
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 判断是否有 token,并且进行认证
        Authentication token = getAuthentication(request);
        if (token == null) {
            chain.doFilter(request, response);
            return;
        }
        // 认证成功
        SecurityContextHolder.getContext().setAuthentication(token);
        chain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header == null || ! header.startsWith("Bearer ")) {
            return null;
        }

        String token = header.split(" ")[1];
        String username = JwtUtil.getUsername(token);
        UserDetails userDetails = null;
        try {
            userDetails = userDetailsService.loadUserByUsername(username);
        } catch (UsernameNotFoundException e) {
            return null;
        }
        if (! JwtUtil.verify(token, username, userDetails.getPassword())) {
            return null;
        }
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

SecurityConfiguration.java

Здесь мы настраиваем Security и реализуем функцию кэширования. Для кэширования этого куска используем официальный готовыйCachingUserDetailsService, единственный недостаток в том, что у него нет публичного метода, мы не можем его нормально инстанцировать, и нам нужна кривая, чтобы спасти страну.Также подробно описан следующий код.

// 开启 Security
@EnableWebSecurity
// 开启注解配置支持
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsServiceImpl;

    // Spring Boot 的 CacheManager,这里我们使用 JCache
    @Autowired
    private CacheManager cacheManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开启跨域
        http.cors()
                .and()
                // security 默认 csrf 是开启的,我们使用了 token ,这个也没有什么必要了
                .csrf().disable()
                .authorizeRequests()
                // 默认所有请求通过,但是我们要在需要权限的方法加上安全注解,这样比写死配置灵活很多
                .anyRequest().permitAll()
                .and()
                // 添加自己编写的两个过滤器
                .addFilter(new JwtAuthenticationFilter(authenticationManager()))
                .addFilter(new JwtAuthorizationFilter(authenticationManager(), cachingUserDetailsService(userDetailsServiceImpl)))
                // 前后端分离是 STATELESS,故 session 使用该策略
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    // 此处配置 AuthenticationManager,并且实现缓存
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 对自己编写的 UserDetailsServiceImpl 进一步包装,实现缓存
        CachingUserDetailsService cachingUserDetailsService = cachingUserDetailsService(userDetailsServiceImpl);
        // jwt-cache 我们在 ehcache.xml 配置文件中有声明
        UserCache userCache = new SpringCacheBasedUserCache(cacheManager.getCache("jwt-cache"));
        cachingUserDetailsService.setUserCache(userCache);
        /*
        security 默认鉴权完成后会把密码抹除,但是这里我们使用用户的密码来作为 JWT 的生成密钥,
        如果被抹除了,在对 JWT 进行签名的时候就拿不到用户密码了,故此处关闭了自动抹除密码。
         */
        auth.eraseCredentials(false);
        auth.userDetailsService(cachingUserDetailsService);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /*
    此处我们实现缓存的时候,我们使用了官方现成的 CachingUserDetailsService ,但是这个类的构造方法不是 public 的,
    我们不能够正常实例化,所以在这里进行曲线救国。
     */
    private CachingUserDetailsService cachingUserDetailsService(UserDetailsServiceImpl delegate) {

        Constructor<CachingUserDetailsService> ctor = null;
        try {
            ctor = CachingUserDetailsService.class.getDeclaredConstructor(UserDetailsService.class);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        Assert.notNull(ctor, "CachingUserDetailsService constructor is null");
        ctor.setAccessible(true);
        return BeanUtils.instantiateClass(ctor, delegate);
    }
}

Конфигурация кэша

Начиная с Ehcache 3, JCache используется единообразно, что является стандартом JSR 107. Многие онлайн-учебники основаны на Ehcache 2, поэтому при обращении к онлайн-учебникам вы можете столкнуться с множеством ошибок.

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

Создал в каталоге ресурсовehcache.xmlдокумент:

<ehcache:config
        xmlns:ehcache="http://www.ehcache.org/v3"
        xmlns:jcache="http://www.ehcache.org/v3/jsr107">

    <ehcache:cache alias="jwt-cache">
        <!-- 我们使用用户名作为缓存的 key,故使用 String -->
        <ehcache:key-type>java.lang.String</ehcache:key-type>
        <ehcache:value-type>org.springframework.security.core.userdetails.User</ehcache:value-type>
        <ehcache:expiry>
            <ehcache:ttl unit="days">1</ehcache:ttl>
        </ehcache:expiry>
        <!-- 缓存实体的数量 -->
        <ehcache:heap unit="entries">2000</ehcache:heap>
    </ehcache:cache>

</ehcache:config>

существуетapplication.propertiesВключите поддержку кеша в:

spring.cache.type=jcache
spring.cache.jcache.config=classpath:ehcache.xml

Единое глобальное исключение

Нам нужно унифицировать форму возврата исключений, чтобы упростить внешние вызовы.

мы обычно используем@RestControllerAdviceдля унификации исключений, но он может управлять исключениями только на уровне контроллера. Исключения, созданные в безопасности, не дойдут до контроллера и не могут быть@RestControllerAdviceзахват, поэтому мы должны преобразоватьErrorController.

@RestController
public class CustomErrorController implements ErrorController {

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping("/error")
    public ResponseEntity handleError(HttpServletRequest request, HttpServletResponse response) {
        return new ResponseEntity(response.getStatus(), (String) request.getAttribute("javax.servlet.error.message"), null);
    }
}

контрольная работа

Попробуйте написать контроллер.Также можно обратиться к способу получения информации о пользователе в моем контроллере.Рекомендуется использовать@AuthenticationPrincipalЭто замечание! ! !

@RestController
public class MainController {

    // 任何人都可以访问,在方法中判断用户是否合法
    @GetMapping("everyone")
    public ResponseEntity everyone() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (! (authentication instanceof AnonymousAuthenticationToken)) {
            // 登入用户
            return new ResponseEntity(HttpStatus.OK.value(), "You are already login", authentication.getPrincipal());
        } else {
            return new ResponseEntity(HttpStatus.OK.value(), "You are anonymous", null);
        }
    }

    @GetMapping("user")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public ResponseEntity user(@AuthenticationPrincipal UsernamePasswordAuthenticationToken token) {
        return new ResponseEntity(HttpStatus.OK.value(), "You are user", token);
    }

    @GetMapping("admin")
    @IsAdmin
    public ResponseEntity admin(@AuthenticationPrincipal UsernamePasswordAuthenticationToken token) {
        return new ResponseEntity(HttpStatus.OK.value(), "You are admin", token);
    }
}

Я также использовал здесь@IsAdminаннотация,@IsAdminПримечания следующие:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public @interface IsAdmin {
}

Это спасает вас от написания длинного списка каждый раз.@PreAuthorize(), и более интуитивным.

FAQ

Как решить проблему истечения срока действия JWT?

мы можемJwtAuthorizationFilterДобавьте некоторый материал, если срок действия пользователя истекает, верните специальный код состояния, внешний интерфейс получает этот код состояния для доступаGET /re_authenticationПросто возьмите старый токен и получите новый токен.

Как аннулировать выпущенные токены, срок действия которых не истек?

Моя личная идея заключается в том, чтобы токен, сгенерированный каждый раз, помещать в кеш, и брать его из кеша при каждом запросе, если токена нет, значит, кеш отбрасывается.