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

实现前后端分离的一个重要问题是如何进行身份验证和授权。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日

相关文章

  • Java并发编程系列之LockSupport的用法

    Java并发编程系列之LockSupport的用法攻略 概述 LockSupport是Java并发编程中提供的一种线程阻塞和唤醒的底层工具,它可以被用于实现高级别的同步工具(如Semaphore、ReentrantLock)等,也可以被用于线程间的通信。 在这篇文章中,我们将会详细介绍LockSupport的使用方法,包括使用park()和unpark()方…

    Java 2023年5月20日
    00
  • 使用IDEA创建Web项目并发布到tomcat的操作方法

    下面是使用IDEA创建Web项目并发布到Tomcat的详细攻略。 1. 配置JDK 使用IDEA开发Web项目需要先配置JDK,可以按照以下步骤进行配置: 打开IDEA,选择File > Project Structure > SDKs。 如果已经有JDK,则可以选择已有的JDK,如果没有,则需要添加JDK。选择左上角的“+”按钮,选择JDK安装…

    Java 2023年5月19日
    00
  • Java虚拟机装载和初始化一个class类代码解析

    Java虚拟机(JVM)的主要任务之一是加载Java类并执行它们的代码。在JVM将class文件转换为可执行代码并在执行时,Java虚拟机会完成以下过程: 类加载 验证类 准备阶段 解析阶段 初始化阶段 以下是这些过程的完整详细解释: 类加载:在Java程序运行时,JVM首先会搜索类加载路径(classpath)来查找并加载字节码文件。类加载器将字节码文件读…

    Java 2023年5月26日
    00
  • java判断字符串String是否为空问题浅析

    Java判断字符串String是否为空问题浅析 在Java中,判断字符串是否为空是一个非常常见的操作。但有时我们在判断时会遇到各种问题,需要进行深入的分析和理解。本篇文章就针对Java判断字符串String是否为空问题进行深入浅出的解析。 什么是空字符串 空字符串是指一个长度为0的字符串,Java中可以使用两种方式表示空字符串:第一种方式是使用””表示,第二…

    Java 2023年5月27日
    00
  • Java从服务器上获取时间动态显示在jsp页面实现思路

    获取服务器上的时间并动态地显示在 JSP 页面上可以通过以下步骤来实现: 在 JSP 页面上引入 Java 提供的日期处理类库 java.util.Date 通过 Java 代码获取当前的时间并将其转化为字符串格式 在 JSP 页面上使用 JavaScript 定时刷新页面内容,动态显示时间 以下是具体的实现步骤和示例代码: 引入 Date 类库 在 JSP…

    Java 2023年5月20日
    00
  • java模拟ATM功能(控制台连接Mysql数据库)

    以下是详细讲解“java模拟ATM功能(控制台连接Mysql数据库)”的完整攻略: 系统要求 JDK 1.8 或以上版本 Mysql 5.0 或以上版本 准备工作 创建一个名为 atm 的 Mysql 数据库 CREATE DATABASE atm; 创建一个名为 users 的表,用于储存 ATM 用户信息 USE atm; CREATE TABLE us…

    Java 2023年5月20日
    00
  • java使用jdbc操作数据库示例分享

    下面是关于“java使用jdbc操作数据库示例分享”的完整攻略: 1. 准备工作 首先,我们需要准备好以下工具和环境:- JDK 1.8 或以上版本- MySQL 数据库- MySQL JDBC 驱动程序- IDE 工具(如 IntelliJ IDEA)或者代码编写器(如 VS Code) 2. 下载并导入JDBC驱动 要使用 JDBC 操作数据库,需要下载…

    Java 2023年6月16日
    00
  • c#桥接模式(bridge结构模式)用法实例

    C#桥接模式(Bridge结构模式)用法实例 什么是C#桥接模式? C#桥接模式,也称为Bridge模式,是一种结构性模式,它将抽象部分与实现部分分离,可以让它们相互独立地变化。这种模式属于结构型模式,它通过提供一个桥接接口,使得抽象和实现可以独立地扩展。 C#桥接模式的应用场景 C#桥接模式主要适用于以下场景: 当一个系统可能有多个角度分类(即多个维度的分…

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