SpringSecurity构建基于JWT的登录认证实现

SpringSecurity构建基于JWT的登录认证实现

本文将介绍如何使用SpringSecurity框架,在基于JWT的前后端分离应用中,实现登录认证功能。

准备工作

在开始介绍实现方案之前,我们需要准备好以下工具和环境:

  • JDK 8 及以上版本
  • Maven 及其配置
  • Spring Boot
  • Spring Security
  • JWT

Step 1: 创建项目

首先,我们需要创建一个Spring Boot项目,可以使用Spring Initializr来快速创建。

Step 2: 添加依赖

在pom.xml文件中,添加以下依赖:

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

Step 3: 配置Spring Security

在Spring Boot项目中,Spring Security的默认配置并不适合前后端分离应用。因此,我们需要自定义Spring Security配置,修改默认配置。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/auth/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new JwtAuthenticationTokenFilter(jwtTokenUtil), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

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

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

在上面的配置中,我们禁用了默认的CSRF保护,开启了自定义的认证过滤器JwtAuthenticationTokenFilter,并使用了无状态的Session管理策略。

Step 4: 实现UserDetailsService

在Spring Security中,UserDetailsService用于通过用户名从数据库中获取用户信息,包括密码和角色等信息。我们需要实现自己的UserDetailsService接口,在其中查询数据库并返回UserDetails对象。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在!");
        }
        return new UserDetail(user);
    }
}

Step 5: 实现JWT

JWT是一种基于JSON的开放标准(RFC 7519),用于在网络上传递声明。JWT可以通过一些算法来实现签名,从而保证数据在传输过程中不被篡改。我们需要实现JWT工具类JwtTokenUtil,来生成和验证JWT Token。

@Component
public class JwtTokenUtil {

    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    private static final String CLAIM_KEY_ROLES = "roles";

    private String secret = "jwt_secret";
    private Long expiration = 604800L;

    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    public String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    public Date getCreatedDateFromToken(String token) {
        Date created;
        try {
            Claims claims = getClaimsFromToken(token);
            created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
        } catch (Exception e) {
            created = null;
        }
        return created;
    }

    public Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            Claims claims = getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }

    private Boolean isTokenExpired(String token) {
        Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    private Claims generateClaims(String username, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(username);
        claims.put(CLAIM_KEY_CREATED, new Date());
        claims.put(CLAIM_KEY_ROLES, roles);
        return claims;
    }

    public String generateToken(String username, List<String> roles) {
        Claims claims = generateClaims(username, roles);
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Boolean canTokenBeRefreshed(String token) {
        return (!isTokenExpired(token));
    }

    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put(CLAIM_KEY_CREATED, new Date());
            refreshedToken = Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(generateExpirationDate())
                    .signWith(SignatureAlgorithm.HS512, secret)
                    .compact();
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        UserDetail userDetail = (UserDetail) userDetails;
        String username = getUsernameFromToken(token);
        return (username.equals(userDetail.getUsername()) && !isTokenExpired(token));
    }
}

Step 6: 自定义认证过滤器

在Spring Security中,认证过滤器UsernamePasswordAuthenticationFilter通常在处理表单登录和Basic认证时使用。在前后端分离应用程序中,我们需要自定义认证过滤器来处理JWT认证。

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private static final String TOKEN_HEADER = "Authorization";

    private JwtTokenUtil jwtTokenUtil;

    public JwtAuthenticationTokenFilter(JwtTokenUtil jwtTokenUtil) {
        this.jwtTokenUtil = jwtTokenUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String tokenHeader = request.getHeader(TOKEN_HEADER);
        if (tokenHeader != null && tokenHeader.startsWith("Bearer ")) {
            String authToken = tokenHeader.substring(7);
            String username = jwtTokenUtil.getUsernameFromToken(authToken);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}

Step 7: 添加登录接口

接下来,我们需要在后台代码中添加登录功能。用户在前端输入账号和密码,后端验证账号密码是否正确。如果认证成功,则返回JWT Token给前端。

@RestController
public class JwtAuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @PostMapping(value = "/auth/login")
    public ResponseResult<LoginSuccessResponse> login(@RequestBody AuthRequest request) {

        // 使用Spring Security进行用户名和密码验证
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword());
        Authentication authentication = authenticationManager.authenticate(authToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 生成JWT Token
        UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
        String token = jwtTokenUtil.generateToken(userDetails.getUsername(), userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList()));

        LoginSuccessResponse successResponse = new LoginSuccessResponse();
        successResponse.setToken(token);
        successResponse.setUsername(userDetails.getUsername());
        successResponse.setRoles(userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList()));
        return ResponseResult.success(successResponse);
    }

    static class AuthRequest {
        private String username;
        private String 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;
        }
    }

    static class LoginSuccessResponse {
        private String username;
        private String token;
        private List<String> roles;

        public String getUsername() {
            return username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        public String getToken() {
            return token;
        }

        public void setToken(String token) {
            this.token = token;
        }

        public List<String> getRoles() {
            return roles;
        }

        public void setRoles(List<String> roles) {
            this.roles = roles;
        }
    }
}

Step 8: 测试

我们可以使用PostMan来测试登录API是否正常工作。将用户名和密码填入请求体中,发送HTTP POST请求到/auth/login,如果认证成功,则会返回JWT Token。

POST /auth/login HTTP/1.1
Host: localhost:8080
Content-Type: application/json

{
  "username": "admin",
  "password": "123456"
}

示例

示例一:GitHub项目

我编写了一个GitHub项目,其中包括了本文中所介绍的完整代码,可以在此处查看:https://github.com/samniya/spring-security-jwt-demo

示例二:CSDN博客

我在CSDN中发表了一篇博客文章,介绍了如何使用Spring Security和JWT实现基于Token的RESTful API认证。可以在此处查看:https://blog.csdn.net/wangsongtao22/article/details/104442766

总结

在本文中,我们介绍了如何使用Spring Security框架和JWT,实现基于Token的RESTful API认证。通过使用自定义的认证过滤器和JWT工具类,我们使得Spring Security与前端SPA的结合更加完美,实现了无状态的认证和授权。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:SpringSecurity构建基于JWT的登录认证实现 - Python技术站

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

相关文章

  • springboot之配置双kafka全过程

    下面是Spring Boot配置双Kafka全过程的攻略: 1. 添加Kafka依赖 在pom.xml文件中添加以下Kafka依赖: <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</ar…

    Java 2023年5月20日
    00
  • Spring Data JPA实现排序与分页查询超详细流程讲解

    下面我来详细讲解一下“Spring Data JPA实现排序与分页查询超详细流程讲解”的完整攻略。本教程共包含以下五个步骤: 引入依赖 在pom.xml文件中加入以下依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>…

    Java 2023年6月2日
    00
  • 浅谈java实现背包算法(0-1背包问题)

    浅谈Java实现背包算法(0-1背包问题) 背包问题 背包问题是计算机科学中的一个经典问题,形式化地说,给定一个有限的物品集合,每一个物品都有一个重量和价值,目标是找到一个所包含物品的子集,使得这些物品的总重量不超过背包的容量,且这些物品的价值最大。 0-1背包问题 0-1背包问题指的是在背包问题的基础上,要求选出的物品的数量必须是0或1。最优解可能有多个,…

    Java 2023年5月19日
    00
  • Java中Builder模式的实现详解

    Java中Builder模式的实现详解 什么是Builder模式 Builder模式是一种创建型设计模式,它可以让你分步骤地创建复杂对象。与工厂模式不同,Builder模式并不是通过单一的方法来创建对象,而是通过多个方法来设置不同的属性,最终创建出一个想要的对象实例。 Builder模式的优点 Builder模式相对于其他创建对象的方式,有如下的优点: 代码…

    Java 2023年5月26日
    00
  • Java编程中的检查型异常与非检查型异常分析

    Java中的异常分为检查型异常和非检查型异常。检查型异常是指在编译期间就需要进行处理,否则代码将无法编译通过。非检查型异常则是指在运行期间发生,不处理也可以编译通过,但是会导致程序出错或崩溃。 检查型异常 检查型异常需要在程序中显式地进行处理。如果不处理,编译时就无法通过。常见的检查型异常有以下几种: IOException 当处理输入输出流时,由于设备或底…

    Java 2023年5月27日
    00
  • SpringBoot开发实战系列之定时器

    Spring Boot 开发实战系列之定时器 在本文中,我们将深入了解 Spring Boot 中定时器的使用。我们将介绍定时器的概念、配置和使用,并提供两个示例。 定时器概念 定时器是指在指定的时间间隔内执行指定的任务。在 Spring Boot 中,我们可以使用 Spring 自带的 @Scheduled 注解来实现定时器的功能。 定时器配置 Sprin…

    Java 2023年5月15日
    00
  • springboot项目中jackson-序列化-处理 NULL教程

    安装Jackson依赖 在 Spring Boot 项目中使用 Jackson 进行数据序列化和反序列化时,需要先在项目中添加 Jackson依赖。 <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-da…

    Java 2023年5月26日
    00
  • SpringMVC实现数据绑定及表单标签

    讲解“SpringMVC实现数据绑定及表单标签”的完整攻略如下: 1. 数据绑定 SpringMVC通过数据绑定将请求参数映射到控制器方法的入参中。实现数据绑定需要在控制器方法入参前面添加@ModelAttribute注解,例如: @RequestMapping(value="/user") public String showUserI…

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