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日

相关文章

  • Java 8新时间日期库java.time的使用示例

    Java 8新时间日期库java.time的使用示例 在Java 8中,引入了全新的时间日期库java.time,它遵循ISO-8601标准,提供了一种方便易用、线程安全、不可变的时间日期处理方式。本文将详细讲解java.time的使用示例,包括日期的创建、日期格式化、时区处理等。 日期的创建 在java.time中,有多种方法可以创建日期,具体如下: 创建…

    Java 2023年5月20日
    00
  • SpringBoot JWT实现token登录刷新功能

    下面就为你详细讲解“SpringBoot JWT实现token登录刷新功能”的完整攻略。 什么是JWT JWT即Json Web Token,是基于JSON格式的令牌,包含有用户的一些身份信息和一些验证信息。在用户登录后,服务器会生成一个JWT给前端返回,在之后的请求中,前端只需在HTTP头中携带该令牌即可实现状态保持。 实现流程 首先,我们需要在项目中引入…

    Java 2023年5月20日
    00
  • Java反射中java.beans包学习总结

    来讲一讲“Java反射中java.beans包学习总结”的攻略吧。 1. 什么是Java反射以及java.beans包 Java中的反射是指:在运行时动态地获取类的信息,比如获取类的构造方法、类的字段信息、类的方法信息等等。这样,我们就可以在运行时获得类的各种信息并进行操作,打破了类的封装性,增加了代码的灵活性。 Java中的java.beans包是操作Ja…

    Java 2023年5月26日
    00
  • Java如何判断整数溢出,溢出后怎么得到提示

    Java中整数类型(int, long等)变量的范围是有限的,当一个变量的数值超出了它的范围时,就会发生整数溢出。溢出的结果与数值运算的结果不同,可能导致程序运行异常,所以我们需要在程序中判断整数是否溢出,并得到提示以确保程序的正确性。 判断整数溢出的方法是通过与最值的比较来实现的。以int类型的整数为例,最大值为2^31-1(即2147483647),最小…

    Java 2023年5月25日
    00
  • java中Pulsar InterruptedException 异常

    Java中Pulsar InterruptedException 异常 当使用Pulsar客户端在Java中进行操作时,可能会遇到InterruptedException异常。在本文中,我们将对该异常进行详细的讲解,包括该异常的原因、如何处理以及代码示例。 什么是InterruptedException异常 InterruptedException是Java…

    Java 2023年5月27日
    00
  • 搭建MyBatis开发环境及基本的CURD介绍

    关于搭建MyBatis开发环境以及基本的CURD介绍,我们需要以下几步: 安装 Java SE环境 首先需要在本地安装好Java SE环境,通常使用官网提供的JDK安装包进行安装,安装完毕之后可以使用 java -version 命令查看安装是否成功。 安装和配置 Maven Maven是一个Java项目管理工具,可以方便地管理Java项目中的依赖关系和构建…

    Java 2023年6月2日
    00
  • Java 多线程实例详解(二)

    Java 多线程实例详解(二) 本文是Java多线程实例系列的第二篇,将进一步介绍Java多线程的实现方式以及相关应用的代码实例。 线程的生命周期 Java线程有5种状态: 新建状态(New):当线程被创建时,它处于新建状态。 就绪状态(Runnable):线程获得了CPU资源,并可以执行,但它还未开始执行它的代码。 运行状态(Running):线程正在执行…

    Java 2023年5月19日
    00
  • Java实现n位数字的全排列

    当需要对n位数字进行全排列时,我们可以使用递归的方法,将这个问题分解成子问题。 具体的步骤如下: 首先定义一个长度为n的数组nums,用来存放数字1~n; 然后定义一个指针start,初始值为0,表示从数组的第一个元素开始进行排列; 定义一个递归函数permute,函数中传入nums数组、长度len、当前指针start,返回值为void; 在permute函…

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