SpringSecurity+JWT实现前后端分离的使用详解

yizhihongxing

实现前后端分离的一个重要问题是如何进行身份验证和授权。Spring Security提供了一个非常方便的方法来处理这个问题,即使用JSON Web Token(JWT)。

JWT是一种用于身份验证和授权的开放标准,它定义了一种紧凑的、自包含的、可自校验的JSON格式来传递信息,通常用于在安全领域的传输而被广泛使用。

下面是SpringSecurity+JWT实现前后端分离的详细攻略:

1. 添加依赖

首先需要添加以下依赖到项目中,可以使用Maven或者Gradle,这里以Maven为例:

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

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

2. 配置Spring Security

配置Spring Security的基本步骤如下:

  1. 创建一个实现了UserDetailsService接口的类,该类用于从数据库中获取用户信息;

  2. 创建一个继承了WebSecurityConfigurerAdapter类的security配置类,并覆盖一些默认配置;

  3. 配置PasswordEncoder用于对用户密码进行加密。

以下是一个简单的security配置类:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/api/authenticate").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager()));
    }
}

在上面的配置中,我们禁用了CSRF防护,对于POST请求中的/authenticate接口我们允许所有人访问,其他所有请求都需要验证用户身份。

3. 实现JWT

在Spring Security的配置类中,我们添加了JWTAuthenticationFilter和JWTAuthorizationFilter。JWTAuthenticationFilter用于在验证用户名和密码之后,生成一个JWT并将其添加到Http的Header中返回给客户端;JWTAuthorizationFilter用于在客户端发送请求时,对JWT进行验证并允许或拒绝访问。

这里我们需要实现两个过滤器:

JWTAuthenticationFilter

JWTAuthenticationFilter中的实现如下:

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req,
                                                HttpServletResponse res) throws AuthenticationException {
        try {
            User creds = new ObjectMapper()
                    .readValue(req.getInputStream(), User.class);

            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            creds.getUsername(),
                            creds.getPassword(),
                            new ArrayList<>())
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest req,
                                            HttpServletResponse res,
                                            FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {

        String token = JWT.create()
                .withSubject(((User) auth.getPrincipal()).getUsername())
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .sign(HMAC512(SECRET.getBytes()));

        res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
    }
}

在JWTAuthenticationFilter中,我们覆盖了attemptAuthentication方法,当用户输入用户名和密码时,这个方法会被调用并使用authenticationManager对用户名和密码进行验证。

在验证成功时,我们使用JWT库生成一个JWT token并将其添加到Http的Header中返回给客户端。

JWTAuthorizationFilter

JWTAuthorizationFilter中的实现如下:

public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws IOException, ServletException {

        String header = req.getHeader(HEADER_STRING);

        if (header == null || !header.startsWith(TOKEN_PREFIX)) {
            chain.doFilter(req, res);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(req);

        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(req, res);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(HEADER_STRING);
        if (token != null) {
            // parse the token.
            String user = JWT.require(HMAC512(SECRET.getBytes()))
                    .build()
                    .verify(token.replace(TOKEN_PREFIX, ""))
                    .getSubject();

            if (user != null) {
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
            }
            return null;
        }
        return null;
    }
}

在JWTAuthorizationFilter中,我们覆盖了doFilterInternal方法,当客户端在Http的Header中发送了一个名为Authorization的JWT token时,这个方法会被调用。

在该方法中,我们获取JWT token并对其进行验证,如果验证成功,将该用户的信息添加到SecurityContextHolder中,允许用户访问所请求的资源。

4. 控制器实现

在控制器中,需要提供一个/authenticate接口,用于验证用户身份并生成JWT token并返回给客户端。

以下是一个简单的控制器示例:

@RestController
@RequestMapping("/api")
public class AuthenticationController {

    private AuthenticationManager authenticationManager;

    @Autowired
    public AuthenticationController(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @PostMapping("/authenticate")
    public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {

        try {
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
            );
        } catch (BadCredentialsException e) {
            throw new Exception("Incorrect username or password", e);
        }

        final UserDetails userDetails = userDetailsService
                .loadUserByUsername(authenticationRequest.getUsername());

        final String jwt = jwtTokenUtil.generateToken(userDetails);

        return ResponseEntity.ok(new AuthenticationResponse(jwt));
    }
}

在上面的控制器中,我们提供了一个/api/authenticate接口,用于验证用户身份。

接口接收一个AuthenticationRequest对象,包含用户名和密码。

如果验证成功,则使用jwtTokenUtil生成JWT token并将其添加到AuthenticationResponse对象中返回给客户端。

5. 示例

以下是一个示例代码:

AuthenticationRequest

public class AuthenticationRequest {

    private String username;
    private String password;

    public AuthenticationRequest() {
    }

    public AuthenticationRequest(String username, String password) {
        this.username = username;
        this.password = password;
    }

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

AuthenticationResponse

public class AuthenticationResponse {

    private final String jwt;

    public AuthenticationResponse(String jwt) {
        this.jwt = jwt;
    }

    public String getToken() {
        return jwt;
    }
}

User

public class User {

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

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

测试用例

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class AuthenticationControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private UserRepository userRepository;

    @Before
    public void setUp() throws Exception {
        userRepository.deleteAll();
        userRepository.save(new User("admin", "password"));
    }

    @Test
    public void should_return_jwt_token_when_authenticate_given_valid_username_and_password() throws Exception {
        AuthenticationRequest authenticationRequest = new AuthenticationRequest("admin", "password");

        mockMvc.perform(post("/api/authenticate")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(authenticationRequest)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.token").isString());
    }

    @Test
    public void should_return_error_when_authenticate_given_invalid_username_and_password() throws Exception {
        AuthenticationRequest authenticationRequest = new AuthenticationRequest("invalid_username", "invalid_password");

        mockMvc.perform(post("/api/authenticate")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(authenticationRequest)))
                .andExpect(status().isUnauthorized());
    }

}

在测试用例中,我们使用MockMvc模拟一个HttpServletRequest并发送给/authenticate接口,使用ObjectMapper将AuthenticationRequest对象转换为JSON格式并将其传送给/authenticate接口。

如果验证成功,我们期望返回200 OK,并且返回的JSON对象应该包含一个名为token的字符串。如果验证失败,我们期望返回401 Unauthorized。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:SpringSecurity+JWT实现前后端分离的使用详解 - Python技术站

(0)
上一篇 2023年5月20日
下一篇 2023年5月20日

相关文章

  • seatunnel 2.3.1全流程部署使用教程

    Seatunnel 2.3.1全流程部署使用教程 简介 Seatunnel是一款基于Socks5协议的加密代理工具,可以实现我们的网络隐私和安全。Seatunnel支持Windows、Linux、macOS等多个平台使用。 本教程将介绍Seatunnel的全流程部署和使用,包括下载安装、配置文件和证书生成、启动使用等。 步骤一:下载Seatunnel 在Se…

    Java 2023年6月2日
    00
  • 消息队列-kafka消费异常问题

    消息队列-kafka消费异常问题主要包括以下几个方面: 消费者异常退出问题 重复消费问题 消费速度慢导致的积压现象 我们将针对以上问题逐一展开讲解,包括其原因和解决方法。 1. 消费者异常退出问题 消费者异常退出问题,主要发生在程序崩溃或机器宕机等情况下。这种情况下,消息队列的消费进度会被打回,并且消息会重新消费一遍,导致重复消费问题。 解决这个问题的方法是…

    Java 2023年5月20日
    00
  • Java IO及BufferedReader.readline()出现的Bug

    关于“Java IO及BufferedReader.readline()出现的Bug”,我们需要注意以下两点: 1. Java IO 中的缓存问题 Java的IO操作是基于缓存进行的,而很多读取函数如BufferedReader. readline()是以换行符作为结束标记的,但是我们在编写代码时常常忽略了特殊情况的处理,导致出现了缓存问题,例如一次读取操作…

    Java 2023年5月27日
    00
  • JNI语言基本知识

    JNI(Java Native Interface)是Java虚拟机提供的一个接口,允许Java程序调用本地C/C++方法或者本地C/C++程序调用Java方法。在进行JNI开发时,需要了解JNI语言的一些基本知识,下面是详细攻略: JNI语言基本知识 1. JNI开发环境搭建 在进行JNI开发前,需要安装C/C++ 编译器和Java开发工具包(JDK)。同…

    Java 2023年5月26日
    00
  • 浅谈Java当作数组的几个应用场景

    浅谈Java当作数组的几个应用场景 Java 数组是一个容器,可以存储一定数量的数据,Java 数组可以包含基本类型(int、short、long、byte、float、double、boolean、char)和引用类型(类、接口、数组)。 Java 数组可以作为各种数据结构的基础,介绍几个 Java 数组的应用场景。 1. 用 Java 数组模拟队列 队列…

    Java 2023年5月26日
    00
  • JAVA中读取文件(二进制,字符)内容的几种方法总结

    下面是题目要求的详细攻略: JAVA中读取文件(二进制,字符)内容的几种方法总结 一、读取二进制文件内容 1. FileInputStream 使用 FileInputStream 可以读取二进制文件的内容。 public static byte[] readContentByFileInputStream(String filePath) throws I…

    Java 2023年5月20日
    00
  • java 反射机制详解及实例代码

    Java反射机制详解 Java反射机制是指在运行时使用Reflection API动态获取类信息、构造对象、调用方法、访问属性等。反射机制在框架开发、ORM映射、动态代理、JavaBean工具、JUnit单元测试等领域有着广泛的应用。 反射机制的特性 Java反射机制具有以下特性: 运行时类型信息:反射机制可以获取类的各种信息,例如类名、父类、接口、方法、属…

    Java 2023年5月23日
    00
  • Java参数传递实现代码及过程图解

    Java参数传递实现代码及过程图解 在Java中,方法传递参数是通过按值传递(pass-by-value)实现的。简单来说,就是在传递参数的时候,将参数的值复制一份给方法内部进行操作,不会直接影响原来的变量值。以下是Java参数传递的一些细节和过程图解: 基本数据类型的参数传递 基本数据类型的参数传递就是将值复制给方法内部进行操作,不会对原来的变量值产生影响…

    Java 2023年5月30日
    00
合作推广
合作推广
分享本页
返回顶部