Spring Boot 2 + Spring Security 5 + JWT 的单页应用 Restful 解决方案

Spring-SPA:单页 ApplicationREST-API 的 Spring Boot 设置 温泉-SPA 单页应用程序/REST-API 的 Spring Boot 设置 Spring Boot 是数据层、REST-API、Spring MVC 和 Javascript 应用程序的完美设置,这些应用程序都内置在一个可运行的 JAR 中! 使用 Cloud Foundry 在大约 30 秒内部署整个堆栈! 立即下载

作者:Smith Cruise

原文链接:https://www.inlighting.org/archives/spring-boot-security-jwt-restful

此前我已经写过一篇类似的教程,但那时候使用了投机的方法,没有尊重 Spring Security 的官方设计,自己并不感到满意。这段时间比较空,故重新研究了一遍。

本项目 GitHub:https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA

老版本:https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA/blob/master/README_OLD.md

特性

  • 使用 JWT 进行鉴权,支持 token 过期

  • 使用 Ehcache 进行缓存,减少每次鉴权对数据库的压力

  • 尽可能贴合 Spring Security 的设计

  • 实现注解权限控制

准备

开始本教程的时候希望对下面知识点进行粗略的了解。

  • 知道 JWT 的基本概念

  • 了解过 Spring Security

我之前写过两篇关于安全框架的问题,大家可以大致看一看,打下基础。

Shiro+JWT+Spring Boot Restful 简易教程:https://github.com/Smith-Cruise/Spring-Boot-Shiro

Spring Boot+Spring Security+Thymeleaf 简单教程:https://github.com/Smith-Cruise/Spring-Boot-Security-Thymeleaf-Demo

本项目中 JWT 密钥是使用用户自己的登入密码,这样每一个 token 的密钥都不同,相对比较安全。

大体思路:

登入:

  1. POST 用户名密码到 \login

  2. 请求到达 JwtAuthenticationFilter 中的 attemptAuthentication() 方法,获取 request 中的 POST 参数,包装成一个 UsernamePasswordAuthenticationToken 交付给 AuthenticationManager 的 authenticate() 方法进行鉴权。

  3. AuthenticationManager 会从 CachingUserDetailsService 中查找用户信息,并且判断账号密码是否正确。

  4. 如果账号密码正确跳转到 JwtAuthenticationFilter 中的 successfulAuthentication() 方法,我们进行签名,生成 token 返回给用户。

  5. 账号密码错误则跳转到 JwtAuthenticationFilter 中的 unsuccessfulAuthentication() 方法,我们返回错误信息让用户重新登入。

请求鉴权:

请求鉴权的主要思路是我们会从请求中的 Authorization 字段拿取 token,如果不存在此字段的用户,Spring Security 会默认会用 AnonymousAuthenticationToken() 包装它,即代表匿名用户。

  1. 任意请求发起

  2. 到达 JwtAuthorizationFilter 中的 doFilterInternal() 方法,进行鉴权。

  3. 如果鉴权成功我们把生成的 Authentication 用 SecurityContextHolder.getContext().setAuthentication() 放入 Security,即代表鉴权完成。此处如何鉴权由我们自己代码编写,后序会详细说明。

准备 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

关于 role 为什么使用 GrantedAuthority 说明下:其实是为了简化代码,直接用了 Security 现成的 role 类,实际项目中我们肯定要自己进行处理,将其转换为 Security 的 role 类。

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 中被移除了,不安全)。

用户名密码权限
jackjack123 存 Bcrypt 加密后ROLE_USER
dannydanny123 存 Bcrypt 加密后ROLE_EDITOR
smithsmith123 存 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,主要就是模仿数据库的操作。

@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;
            }
        }
    }

Spring Security 改造

登入这块,我们使用自定义的 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));
            }
        }
    }

privatevoidhandleResponse() 此处处理的方法不是很好,我的想法是跳转到控制器中进行处理,但是这样鉴权成功的 token 带不过去,所以先这么写了,有点复杂。

这个过滤器处理每个请求鉴权,我们选择继承 BasicAuthenticationFilter ,考虑到 Basic 认证和 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 ,唯独的缺点就是它没有 public 方法,我们不能正常实例化,需要曲线救国,下面代码也有详细说明。

// 开启 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 配置

Ehcache 3 开始,统一使用了 JCache,就是 JSR107 标准,网上很多教程都是基于 Ehcache 2 的,所以大家可能在参照网上的教程会遇到很多坑。

JSR107:emm,其实 JSR107 是一种缓存标准,各个框架只要遵守这个标准,就是现实大一统。差不多就是我不需要更改系统代码,也能随意更换底层的缓存系统。

在 resources 目录下创建 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 来统一异常,但是它只能管理 Controller 层面抛出的异常。Security 中抛出的异常不会抵达 Controller,无法被 @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 携带老的 token 重新拿一个新的 token 即可。

如何作废已颁发未过期的 token?

我个人的想法是把每次生成的 token 放入缓存中,每次请求都从缓存里拿,如果没有则代表此缓存报废。

好文!必须点赞

spring security oauth2 实现jwt sso 该资源实现了一个sso单点登陆的功能,类似于在淘宝网登陆之后可以不需要登陆天猫即可访问天猫网;使用了spring security oauth2以及jwt。 立即下载

相关推荐

springboot整合security与jwt

springboot2.0整合security与jwt, 还整合了redis,与swagger-ui

springsecurity Oauth2.0 + jwt 极简入门demo

springsecurity Oauth2.0 服务器和客户端 极简入门demo

asfasfasgjkl的博客 1713

customerprofiler:客户分析,React,Spring Boot 2单页Web应用程序

客户分析应用 后端-Spring Boot 2框架前端-React 运行步骤 进入页面: 在本地计算机上运行的步骤 构建--- c:\ customerprofiler> gradlew构建 运行--- c:\ customerprofiler> gradlew bootrun 假设条件 仅显示一个分类。 所有分类均针对用户选择的一个月 Txns将在本月显示 余额是所有月份的总余额 当交易大于> 50%但不等于50%时,将对人员进行分类 在下午12点而非12点-12:59 PM之后完成交易时,将确定下午人员交易 如果交易金额超过1000美元且不等于1000美元,则将Big Spender归类 如果交易量大于50%且交易时间早于或等于12PM或直到中午12.59PM的整个小时,则将早起人员归类 潜在节省者少于费用的25%,但不等于25% 当7天内的总支出大于所存入金额的75%(但不等于

案例1 springsecurity+ jwt ,案例2 springsecurity+jwt +oauth2 ,案例3 springsecurity+jwt+oauth2+zull网关

动态权限控制 Rbac权限表设计 首先不管是实现哪种,都要基于rbac表设计,用户---中间表---角色---中间表---权限 案例1 springsecurity+ jwt springsecurity是一个安全框架 jwt (JSON WEB Token ) 优点: 无需再服务器存放用户的数据,减去服务器端压力 轻量级、json风格比较简单 跨语言 缺点: Token一旦生成后期无法修改: 无法更新token有效期 无法销毁一个token JWT组成: 第一部分:hea.

qq_39564710的博客 838

Windows10完美驱动Apple Studio Display:从雷电接口到亮度调节全攻略

本文详细介绍了如何在Windows10系统上完美驱动Apple Studio Display,涵盖雷电接口兼容性、亮度调节及色彩管理等关键问题。通过实测对比不同转接方案和显卡兼容性,推荐使用贝尔金VR线和NVIDIA GTX-1660显卡,并提供了BootCamp驱动安装和亮度调节的终极解决方案,帮助用户实现最佳显示效果。

elastic6hunter的博客 1122

告别网络卡顿!保姆级教程:用国内镜像源5分钟搞定ESP32开发环境(Arduino IDE)

本文提供了一份保姆级教程,教你如何利用国内镜像源在5分钟内快速搭建ESP32开发环境(Arduino IDE),告别网络卡顿问题。教程详细介绍了镜像源配置、开发板安装、测试程序烧录等步骤,并包含与巴法云对接的实战案例,帮助开发者高效完成物联网项目。

weixin_42536058的博客 593

Spring Boot 2 + Spring Security 5 + JWT 的 Restful简易教程!(1)

现在正是金三银四的春招高潮,前阵子小编一直在搭建自己的网站,并整理了全套的**【一线互联网大厂Java核心面试题库+解析】:包括Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等**《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

2401_83916204的博客 707

PAM4信号测试实战:从眼图分析到误码率测量的技术演进

在高速数字系统设计中,信号完整性测试是确保数据传输可靠性的核心环节。其基本原理在于通过时域和频域分析,精确评估信号在传输过程中的质量衰减与失真。随着数据速率向56Gbps及以上演进,传统的NRZ编码面临带宽瓶颈,而PAM4调制技术通过在每个符号周期内承载更多比特信息,成为提升信道效率的关键路径。这项技术的工程价值在于,它允许在相同带宽下实现翻倍的数据吞吐量,这对于数据中心互联、PCIe 6.0及800G以太网等高速接口至关重要。然而,PAM4信号电平间距更小,对噪声和抖动极为敏感,导致其原始误码率显著升高,

weixin_42522817的博客 659

spring boot2 + springsecurity5 + jwt 例子

准备 开始本教程的时候希望对下面知识点进行粗略的了解。 知道 JWT 的基本概念 了解过 Spring Security 本项目中JWT密钥是使用用户自己的登入密码,这样每一个token的密钥都不同,相对比较安全。 大体思路: 登入: POST 用户名密码到 \login 请求到达JwtAuthenticationFilter中的attemptAuthentication()方法,获取 request 中的 POST 参数,包装成一个Usernam...

神盾局码农 396

EM4095低频RFID曼彻斯特解码实战:从原理图到MCU代码实现

本文详细介绍了EM4095低频RFID读卡器的设计与实现,从芯片工作原理、天线设计到曼彻斯特解码算法。重点讲解了基于STM32的MCU代码实现,包括定时器捕获、数字滤波和错误处理,为开发125kHz RFID系统提供实用指南。

oil88的博客 717

基于STM32的HC-SR04超声波模块测距(新手级)

本人是嵌入式新手,学习了STM32的部分内容,最近想找些项目做,然后顺便学习了一下HC-SR04超声波模块,学习过程中也参考了一些大佬的文章。本文记录了自己的学习方法和学习过程,同时也希望能和各位大佬交流讨论。

weixin_63758495的博客 2206

Spring Boot 2 + Spring Security 5 + JWT 的 Restful简易教程!

给大家送一个小福利附高清脑图,高清知识点讲解教程,以及一些面试真题及答案解析。送给需要的提升技术、准备面试跳槽、自身职业规划迷茫的朋友们。《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!也去过华为、OPPO等大厂,18年进入阿里一直到现在。**深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

2401_83916241的博客 1101

已过tiktok校招客户端,面经分享

面试的话, 一天面完3面, 效率很高, 三天之后就发了offer 字节邮件里面也说了不能泄露面试题, 那我就把我秋招参与美团,字节,腾讯,阿里面试被问到的全部问题总结着写出来了, 这其实是我每一轮面试完之后我都会总结哪里答对了, 哪里没有答对, 总结了一大堆问题出来. 答案未必正确, 我只是给自己写了个简单的一句话概括, 可能不够完善, 建议看官自行回答一下这些问题 二本非科班, 暑假在字节实习过, 不过不是tiktok部门, 因此秋招就没啥时间准备面试了, 只投了几个大厂, 顺便也在字节跨部门转正,

a1472750149的博客 1308

jwt配置 restful_Spring Boot 2 + Spring Security 5 + JWT 的单页应用 Restful 解决方案

作者:Smith Cruise原文链接:https://www.inlighting.org/archives/spring-boot-security-jwt-restful此前我已经写过一篇类似的教程,但那时候使用了投机的方法,没有尊重 Spring Security 的官方设计,自己并不感到满意。这段时间比较空,故重新研究了一遍。本项目 GitHub:https://github....

weixin_39847728的博客 108

AD9361 官方例程

文章目录一、官方资料1.1 介绍1.2 下载 和 使用1.3 相关资料二、步骤2.1 在Vivado中构建HDL2.2 在SDK中构建No- OS总结 一、官方资料 1.1 介绍 HDL参考设计介绍:AD9361 HDL Reference Designs 无操作系统介绍:AD9361 No-OS Software 1.2 下载 和 使用 HDL下载:Download HDL 在Vivado中构建HDL:Building HDL No-OS下载:Download No-OS 1.3 相关资料 AD

weixin_44630490的博客 3万+

RPA 财务共享中心与RPA赋能

在和集团财务官交流的时候,客户经常会提到FSSC,其中有准备建立FSSC,正在建立FSSC,以及已建成FSSC正使用的,当中会有些对FSSC的疑虑和问题,所以花费些时间对FSSC进行了深入调研,从而也有幸拜读了王兴山、陈剑、梅震、钱自严等专业人士写的几本关于FSSC的书,对FSSC的前因后果有了更深入的认识,同时结合自己在FSSC的一些项目经验,故做了本次分享。 本文章主要分享财务共享中心以及R...

weixin_40244959的博客 2757

Spring Boot 2 + Spring Security 5 + JWT 的单页应用Restful解决方案

准备 项目GitHub:github.com/Smith-Cruis… 我之前写过两篇关于安全框架的问题,大家可以大致看一看,打下基础。 Shiro+JWT+Spring Boot Restful简易教程 Spring Boot+Spring Security+Thymeleaf 简单教程 在开始前你至少需要了解 Spring Security 的基本配置和 JWT 机制。 一些关于 Maven ...

weixin_34010566的博客 475

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

SpringBoot 2 + Spring Security 5 + JWT 的单页应用 Restful 解决方案

此前我已经写过一篇类似的教程,但那时候使用了投机的方法,没有尊重 Spring Security 的官方设计,自己并不感到满意。这段时间比较空,故重新研究了一遍。 特性 使用 JWT 进行鉴权,支持 token 过期 使用 Ehcache 进行缓存,减少每次鉴权对数据库的压力 尽可能贴合 Spring Security 的设计 实现注解权限控制 准备 开始本教程的时候希望对下面知识点进行粗略的了解。 知道 JWT 的基本概念 了解过 Spring Security 本项目中 JWT 密钥是使用用户自

LoveSummer 1987

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

Spring Boot + Security + JWT 项目复盘:从零构建一个安全的 RESTful API

本文介绍了一个基于SpringBoot和JWT的用户-水果管理系统,采用三层架构实现用户认证和授权功能。系统核心模块包括用户注册/登录/信息更新/注销,以及水果添加和分页查询功能。技术栈整合了SpringSecurity、MyBatis、MySQL和PageHelper,通过JwtAuthenticationFilter实现无状态认证。重点阐述了安全配置、JWT工具类、统一响应格式等关键组件,展示了如何通过SpringSecurity上下文获取当前用户信息进行业务操作。系统采用BCrypt密码加密和事务管理

m0_58211807的博客 1379

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

使用Spring Boot与Spring Security构建安全的RESTful API

本文详细介绍了如何使用Spring Boot和Spring Security构建安全的RESTful API,并结合JWT实现身份验证与授权。通过实际代码示例,展示了从零开始搭建一个安全的后端服务的完整流程。

intwei的博客 1155

构建安全的RESTful API:Spring Boot与Spring Security整合JWT

JWT是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。它由三部分组成:Header、Payload和Signature。JWT的主要优势在于其无状态性,适合分布式系统的身份验证。本文详细介绍了如何利用Spring Boot和Spring Security整合JWT实现安全的RESTful API。通过代码示例和架构设计,开发者可以快速掌握相关技术。在实际项目中,还可以进一步优化,如添加角色权限控制、刷新令牌机制等。

intwei的博客 875

构建安全的RESTful API:Spring Boot与Spring Security实战

Spring Boot是一个基于Spring框架的快速开发框架,通过自动配置和约定优于配置的原则,简化了Spring应用的开发和部署。通过实现@Service@Autowired@Override本文介绍了如何使用Spring Boot和Spring Security构建安全的RESTful API,从基础配置到高级特性的实现。通过合理的配置和扩展,可以满足不同场景下的安全需求。

intwei的博客 560

使用Spring Boot与Spring Security构建安全的RESTful API:JWT与OAuth2实战

本文详细介绍了如何使用Spring Boot和Spring Security构建安全的RESTful API,涵盖了JWT和OAuth2的集成与实战应用。通过本文的学习,读者可以掌握如何在项目中实现认证与授权功能,从而提升应用的安全性。

intwei的博客 311
上一篇: 开源博客项目eblog完整搭建教程!
下一篇: 绝了!Dataway让SpringBoot不在需要Controller、Service、DAO、Mapper了
Java思维导图
博客等级 码龄10年 306粉丝 71原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值