关于intellij-idea:Spring-Boot-2-Spring-Security-5-JWT-的-Restful-简易教程

3次阅读

共计 16834 个字符,预计需要花费 43 分钟才能阅读完成。

筹备

开始本教程的时候心愿对上面知识点进行粗略的理解。

  • 晓得 JWT 的基本概念
  • 理解过 Spring Security

本我的项目中 JWT 密钥是应用用户本人的登入明码,这样每一个 token 的密钥都不同,绝对比拟平安。

大体思路:

登入:

  1. POST 用户名明码到 \login
  2. 申请达到 JwtAuthenticationFilter 中的 attemptAuthentication() 办法,获取 request 中的 POST 参数,包装成一个 UsernamePasswordAuthenticationToken 交付给 AuthenticationManagerauthenticate() 办法进行鉴权。
  3. AuthenticationManager 会从 CachingUserDetailsService 中查找用户信息,并且判断账号密码是否正确。
  4. 如果账号密码正确跳转到 JwtAuthenticationFilter 中的 successfulAuthentication() 办法,咱们进行签名,生成 token 返回给用户。
  5. 账号密码谬误则跳转到 JwtAuthenticationFilter 中的 unsuccessfulAuthentication() 办法,咱们返回错误信息让用户从新登入。

申请鉴权:

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

  1. 任意申请发动
  2. 达到 JwtAuthorizationFilter 中的 doFilterInternal() 办法,进行鉴权。
  3. 如果鉴权胜利咱们把生成的 AuthenticationSecurityContextHolder.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 中被移除了,不平安)。

用户名 明码 权限
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,次要就是模拟数据库的操作。

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

private void handleResponse() 此处解决的办法不是很好,我的想法是跳转到控制器中进行解决,然而这样鉴权胜利的 token 带不过来,所以先这么写了,有点简单。

JwtAuthorizationFilter.java

这个过滤器解决每个申请鉴权,咱们抉择继承 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 放入缓存中,每次申请都从缓存里拿,如果没有则代表此缓存报废。

我的项目地址:https://github.com/Smith-Crui…

本文首发于公众号:Java 版 web 我的项目,欢送关注获取更多精彩内容

正文完
 0