使用Spring Security和JWT保护REST API实战源码

栏目: Java · 发布时间: 6年前

内容简介:设计REST API时,必须考虑如何保护REST API,在基于Spring的应用程序中,Spring Security是一种出色的身份验证和授权解决方案,它提供了几种保护REST API的选项。最简单的方法是使用HTTP Basic,当你启动基于Spring Boot的应用程序时,默认情况下会激活它,这有利于开发,可在开发阶段经常使用,但不建议在生产环境中使用。Spring Session(使用Spring Security)提供了一个简单的策略来创建和验证基于头的令牌(会话ID),它可以用于保护RES

设计REST API时,必须考虑如何保护REST API,在基于Spring的应用程序中,Spring Security是一种出色的身份验证和授权解决方案,它提供了几种保护REST API的选项。

最简单的方法是使用HTTP Basic,当你启动基于Spring Boot的应用程序时,默认情况下会激活它,这有利于开发,可在开发阶段经常使用,但不建议在生产环境中使用。

Spring Session(使用Spring Security)提供了一个简单的策略来创建和验证基于头的令牌(会话ID),它可以用于保护RESTful API。

除此之外,Spring Security OAuth(Spring Security下的子项目)提供OAuth授权的完整解决方案,包括OAuth2协议中定义的所有角色的实现,例如授权服务器,资源服务器,OAuth2客户端等,Spring Cloud在其子项目Spring Cloud Security中给OAuth2客户端增加了单点登录功能,在基于Spring Security OAuth的解决方案中,访问令牌的内容可以是签名的JWT令牌或不透明值,我们必须遵循标准OAuth2授权流程来获取访问令牌。

对于那些没有计划将自己API暴露给第三方应用程序的资源完全拥有者来说,基于JWT令牌的简单授权更简单合理(我们不需要管理第三方客户端应用程序的凭据)。

Spring Security本身并没有提供这样的选项,幸运的是,通过将我们的自定义过滤器混合到Spring Security Filter Chain中来实现它并不困难。在这篇文章中,我们将创建这样一个自定义JWT身份验证解决方案。

在此示例应用程序中,可以将基于自定义JWT令牌的身份验证流程指定为以下步骤:

1. 从身份验证端点获取基于JWT的令牌,例如/auth/signin。

2. 从身份验证结果中提取令牌。

3. 将HTTP标头Authorization值设置为Bearer jwt_token。

4. 然后发送一个访问受保护资源的请求。

5. 如果请求的资源受到保护,Spring Security将使用我们的自定义Filter来验证JWT令牌,并构建一个Authentication对象,把它放入SecurityContextHolder以完成身份验证流程。

6. 如果JWT令牌有效,它将把请求的资源返回给客户端。

生成项目框架

创建新Spring Boot项目的最快方法是使用Spring Initializr生成基本代码。

打开浏览器,转到http://start.spring.io,在Dependencies字段中,选择Web,Security,JPA,Lombok,然后单击Generate按钮或按ALT + ENTER键以生成项目框架代码。

等待一段时间下载生成的代码,完成后,将zip文件解压缩到本地系统。

打开你喜欢的IDE,例如Intellij IDEA,NetBeans IDE,然后导入它。

创建示例REST API

在此应用程序中,我们将公开车辆资源的REST API。

/vehicles POST {name:'title'}

/vehicles/{id} GET 200, {id:'1', name:'title'}

/vehicles/{id} PUT {name:'title'}

/vehicles/{id} DELETE

创建JPA实体Vehicle。

@Entity
@Table(name="vehicles")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Vehicle implements Serializable {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id ;

  @Column
  private String name;
}

创建JPA存储库:

public interface VehicleRepository extends JpaRepository<Vehicle, Long> {
}

创建一个Spring MVC basec Controller来公开REST API。

@RestController
@RequestMapping("/v1/vehicles")
public class VehicleController {

    private VehicleRepository vehicles;

    public VehicleController(VehicleRepository vehicles) {
        this.vehicles = vehicles;
    }


    @GetMapping("")
    public ResponseEntity all() {
        return ok(this.vehicles.findAll());
    }

    @PostMapping("")
    public ResponseEntity save(@RequestBody VehicleForm form, HttpServletRequest request) {
        Vehicle saved = this.vehicles.save(Vehicle.builder().name(form.getName()).build());
        return created(
            ServletUriComponentsBuilder
                .fromContextPath(request)
                .path("/v1/vehicles/{id}")
                .buildAndExpand(saved.getId())
                .toUri())
            .build();
    }

    @GetMapping("/{id}")
    public ResponseEntity get(@PathVariable("id") Long id) {
        return ok(this.vehicles.findById(id).orElseThrow(() -> new VehicleNotFoundException()));
    }


    @PutMapping("/{id}")
    public ResponseEntity update(@PathVariable("id") Long id, @RequestBody VehicleForm form) {
        Vehicle existed = this.vehicles.findById(id).orElseThrow(() -> new VehicleNotFoundException());
        existed.setName(form.getName());

        this.vehicles.save(existed);
        return noContent().build();
    }

    @DeleteMapping("/{id}")
    public ResponseEntity delete(@PathVariable("id") Long id) {
        Vehicle existed = this.vehicles.findById(id).orElseThrow(() -> new VehicleNotFoundException());
        this.vehicles.delete(existed);
        return noContent().build();
    }
}

这很简单而且不用动脑。我们定义了VehicleNotFoundException,如果相关id车辆未找到将抛出这个错误。

创建一个简单的异常处理程序来处理自定义异常。

@RestControllerAdvice
@Slf4j
public class RestExceptionHandler {

    @ExceptionHandler(value = {VehicleNotFoundException.class})
    public ResponseEntity vehicleNotFound(VehicleNotFoundException ex, WebRequest request) {
        log.debug("handling VehicleNotFoundException...");
        return notFound().build();
    }
}	

创建一个CommandLineRunnerbean以在应用程序启动阶段初始化一些车辆数据。

@Component
@Slf4j
public class DataInitializer implements CommandLineRunner {

    @Autowired
    VehicleRepository vehicles;


    @Override
    public void run(String... args) throws Exception {
        log.debug("initializing vehicles data...");
        Arrays.asList("moto", "car").forEach(v -> this.vehicles.saveAndFlush(Vehicle.builder().name(v).build()));

        log.debug("printing all vehicles...");
        this.vehicles.findAll().forEach(v -> log.debug(" Vehicle :" + v.toString()));
    }
}

通过在终端中执行命令行mvn spring-boot:run运行,或直接在IDE中运行类来运行应用程序。

打开终端,用于curl测试API:

>curl http://localhost:8080/v1/vehicles
[ {
  "id" : 1,
  "name" : "moto"
}, {
  "id" : 2,
  "name" : "car"
} ]

Spring Data Rest能直接通过Repository接口公开API。

@RepositoryRestResource在现有VehicleRepository界面上添加注释。

@RepositoryRestResource(path = "vehicles", collectionResourceRel = "vehicles", itemResourceRel = "vehicle")
public interface VehicleRepository extends JpaRepository<Vehicle, Long> {
}

重新启动应用程序并尝试访问http://localhost:8080/vehicles

curl -X GET http://localhost:8080/vehicles 
{
  "_embedded" : {
    "vehicles" : [ {
      "name" : "moto",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/vehicles/1"
        },
        "vehicle" : {
          "href" : "http://localhost:8080/vehicles/1"
        }
      }
    }, {
      "name" : "car",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/vehicles/2"
        },
        "vehicle" : {
          "href" : "http://localhost:8080/vehicles/2"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/vehicles{?page,size,sort}",
      "templated" : true
    },
    "profile" : {
      "href" : "http://localhost:8080/profile/vehicles"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 2,
    "totalPages" : 1,
    "number" : 0
  }
}

这里利用Spring HATEOAS项目来暴露更丰富的REST API,这些API属于Richardson Mature Model Level 3(自我文档)。

保护REST API

现在我们将创建一个基于JWT令牌的自定义身份验证过滤器来验证JWT令牌。

JwtTokenFilter为JWT令牌验证创建过滤器名称。

public class JwtTokenFilter extends GenericFilterBean {

    private JwtTokenProvider jwtTokenProvider;

    public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
        throws IOException, ServletException {

        String token = jwtTokenProvider.resolveToken((HttpServletRequest) req);
        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication auth = token != null ? jwtTokenProvider.getAuthentication(token) : null;
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.doFilter(req, res);
    }

}

它使用JwtTokenProvider处理JWT,例如生成JWT令牌,解析JWT声明。

@Component
public class JwtTokenProvider {

    @Value("${security.jwt.token.secret-key:secret}")
    private String secretKey = "secret";

    @Value("${security.jwt.token.expire-length:3600000}")
    private long validityInMilliseconds = 3600000; // 1h

    @Autowired
    private UserDetailsService userDetailsService;

    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    public String createToken(String username, List<String> roles) {

        Claims claims = Jwts.claims().setSubject(username);
        claims.put("roles", roles);

        Date now = new Date();
        Date validity = new Date(now.getTime() + validityInMilliseconds);

        return Jwts.builder()//
            .setClaims(claims)//
            .setIssuedAt(now)//
            .setExpiration(validity)//
            .signWith(SignatureAlgorithm.HS256, secretKey)//
            .compact();
    }

    public Authentication getAuthentication(String token) {
        UserDetails userDetails = this.userDetailsService.loadUserByUsername(getUsername(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    public String getUsername(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    public String resolveToken(HttpServletRequest req) {
        String bearerToken = req.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7, bearerToken.length());
        }
        return null;
    }

    public boolean validateToken(String token) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);

            if (claims.getBody().getExpiration().before(new Date())) {
                return false;
            }

            return true;
        } catch (JwtException | IllegalArgumentException e) {
            throw new InvalidJwtAuthenticationException("Expired or invalid JWT token");
        }
    }

}

创建一个独立的Configurer类来进行设置JwtTokenFilter。

public class JwtConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    private JwtTokenProvider jwtTokenProvider;

    public JwtConfigurer(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider);
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

在我们的应用程序作用域中应用此配置器SecurityConfig。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    JwtTokenProvider jwtTokenProvider;

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //@formatter:off
        http
            .httpBasic().disable()
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                .authorizeRequests()
                .antMatchers("/auth/signin").permitAll()
                .antMatchers(HttpMethod.GET, "/vehicles/**").permitAll()
                .antMatchers(HttpMethod.DELETE, "/vehicles/**").hasRole("ADMIN")
                .antMatchers(HttpMethod.GET, "/v1/vehicles/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .apply(new JwtConfigurer(jwtTokenProvider));
        //@formatter:on
    }
}

要启用Spring Security,我们必须在运行时提供自定义UserDetailsService这个bean:

@Component
public class CustomUserDetailsService implements UserDetailsService {

    private UserRepository users;

    public CustomUserDetailsService(UserRepository users) {
        this.users = users;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return this.users.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("Username: " + username + " not found"));
    }
}

该CustomUserDetailsService试图以用户名为查询参数从数据库中获取用户数据。

User是一个标准的JPA实体,为了简化工作,它还实现了Spring Security特定的UserDetails接口。

@Entity
@Table(name="users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long id;

    @NotEmpty
    private String username;

    @NotEmpty
    private String password;

    @ElementCollection(fetch = FetchType.EAGER)
    @Builder.Default
    private List<String> roles = new ArrayList<>();

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.roles.stream().map(SimpleGrantedAuthority::new).collect(toList());
    }

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

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

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

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

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

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

创建为User实体创建一个Repository接口:

public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByUsername(String username);

}

创建一个控制器来验证用户:

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    JwtTokenProvider jwtTokenProvider;

    @Autowired
    UserRepository users;

    @PostMapping("/signin")
    public ResponseEntity signin(@RequestBody AuthenticationRequest data) {

        try {
            String username = data.getUsername();
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, data.getPassword()));
            String token = jwtTokenProvider.createToken(username, this.users.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("Username " + username + "not found")).getRoles());

            Map<Object, Object> model = new HashMap<>();
            model.put("username", username);
            model.put("token", token);
            return ok(model);
        } catch (AuthenticationException e) {
            throw new BadCredentialsException("Invalid username/password supplied");
        }
    }
}

创建端点以获取当前用户信息。

@RestController()
public class UserinfoController {

    @GetMapping("/me")
    public ResponseEntity currentUser(@AuthenticationPrincipal UserDetails userDetails){
        Map<Object, Object> model = new HashMap<>();
        model.put("username", userDetails.getUsername());
        model.put("roles", userDetails.getAuthorities()
            .stream()
            .map(a -> ((GrantedAuthority) a).getAuthority())
            .collect(toList())
        );
        return ok(model);
    }
}

当前用户通过身份验证后,@AuthenticationPrincipal将绑定到当前主体。

在我们的初始化类中添加两个用于测试目的的用户。

@Component
@Slf4j
public class DataInitializer implements CommandLineRunner {

  //...

    @Autowired
    UserRepository users;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public void run(String... args) throws Exception {
        //...

        this.users.save(User.builder()
            .username("user")
            .password(this.passwordEncoder.encode("password"))
            .roles(Arrays.asList( "ROLE_USER"))
            .build()
        );

        this.users.save(User.builder()
            .username("admin")
            .password(this.passwordEncoder.encode("password"))
            .roles(Arrays.asList("ROLE_USER", "ROLE_ADMIN"))
            .build()
        );

        log.debug("printing all users...");
        this.users.findAll().forEach(v -> log.debug(" User :" + v.toString()));
    }
}

现在用于curl尝试此身份验证流程。

通过user/password登录:

curl -X POST http://localhost:8080/auth/signin -H "Content-Type:application/json" -d "{\"username\":\"user\", \"password\":\"password\"}"
{
  "username" : "user",
  "token" : "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwicm9sZXMiOlsiUk9MRV9VU0VSIl0sImlhdCI6MTUyNDY0OTI4OSwiZXhwIjoxNTI0NjUyODg5fQ.Lj1w6vPJNdJbcY6cAhO3DbkgCAqpG7lzztzUeKMyNyE"
}

将token值放入HTTP标头Authorization,将其值设置为Bearer token,然后访问当前用户信息。

curl -X GET http://localhost:8080/me -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwicm9sZXMiOlsiUk9MRV9VU0VSIl0sImlhdCI6MTUyNDY0OTI4OSwiZXhwIjoxNTI0NjUyODg5fQ.Lj1w6vPJNdJbcY6cAhO3DbkgCAqpG7lzztzUeKMyNyE"
{
  "roles" : [ "ROLE_USER" ],
  "username" : "user"
}

github中的源代码 ,它还包括使用JUnit,Spring Boot Test,RestAssured等的测试代码。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Making Things See

Making Things See

Greg Borenstein / Make / 2012-2-3 / USD 39.99

Welcome to the Vision Revolution. With Microsoft's Kinect leading the way, you can now use 3D computer vision technology to build digital 3D models of people and objects that you can manipulate with g......一起来看看 《Making Things See》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

html转js在线工具
html转js在线工具

html转js在线工具