springboot使用JWT实现会话认证

最近在做一个项目的收尾,安全检测小组提出了一些需求:

在通信双方建立连接之前,应用系统应利用密码技术进行会话初始化验证,并对通信过程中的敏感信息字段进行加密。

本项目是springboot项目,主要功能是一个管理后台,为了解决这个需求,第一思路就是用户登陆之后给用户客户端一个码,用户客户端需要调用接口的时候带上这个码就可以进行访问了。

这样做是可以的,但是遇到的问题是这个接口都写完了,如果一个一个改,是很麻烦的,我上网查了一下,发现springboot的项目可以引入JWT去解决,下面详细地讲一下如何通过JWT去实现安全小组提出的需求。

打开pom文件,先把依赖加进去,工欲善其事必先利其器。

   <!--加密-->
        <dependency>
            <groupId>com.github.ulisesbocchio</groupId>
            <artifactId>jasypt-spring-boot-starter</artifactId>
            <version>3.0.4</version>
        </dependency>
        <!--spring security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

  这其实和以前登陆的思路是一样的,前端输入账号和密码,后端进行匹配验证。那JWT是怎么做的呢,

       1.以前是有一套用户的数据,现在也是要的,编写一个类重点是实现UserDetailsService接口。为什么要实现UserDetailsService接口,因为实现了UserDetailsService接口才可以更加方便的实现JWT后面的操作,减少很多自己原本需要做的东西

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.ArrayList;

@Component
public class MyUserDetailsService   implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s)   throws UsernameNotFoundException {
        //假数据 
        //具体的业务逻辑--例如数据库中获取账户信息详细信息等等 return new User( "root" ,  "admin" ,  new ArrayList<>()); } }

  这个类实现了UserDetailsService接口,主要是用于入参 s(用户名),然后返回用户的详细信息,主要是new User(用户名, 密码, new ArrayList<>())。

       ok到下一步,下一步就是后端匹配完用户准确之后,需要返回一个token回去,以后前端就拿着这个token问你讨数据。那这个token就靠下面这代码完成

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

//用于生成和解析jwt
@Component
public class JwtUtil {

    private String SECRET_KEY = "secret";  // 在生产环境中,使用强加密的密钥

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    public String generateToken(String username) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, username);
    }

    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10小时有效期
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    public Boolean validateToken(String token, String username) {
        final String extractedUsername = extractUsername(token);
        return (extractedUsername.equals(username) && !isTokenExpired(token));
    }
}

这个代码可以直接复制,我也是复制chatgpt的,有两个地方要留意哈,第一个是private String SECRET_KEY = "secret";这个是密钥,加密用,按照真实情况修改,但是这个应该是要写再配置文件里面,当然配置文件也是需要加密的,这个我有时间再写一下,在开发中也是比较常用的。另外是这个setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10小时有效期,设置token的有效期啊,就是多久需要重新登陆,我个认为有效期可以有效的解决重放攻击,但是这个也会在之后写一下具体的方案,估计是会专门单开一个安全的章节去写,系统的写一下。

ok那现在是匹配用户的类写了,生成token的类也写了,剩下的就是校验前端发送过来的token了

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

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

        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {

                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken
                        .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}

这个类主要是用于校验前端发送过来的token

ok那现在基本是齐活了,生成token的类,检验token的类,还有用户信息的类,现在需要把他们组装起来。

port org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Override
//装配用户信息类
protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailsService); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); }
   @Bean
//配置跨域类,security会自动识别CorsFilter的类进行使用,如果存在多个则会优先使用corsFilter名称的类
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true); // 允许发送Cookie信息
config.addAllowedOriginPattern("*"); // 允许所有来源的请求
config.addAllowedHeader("*"); // 允许所有请求头
config.addAllowedMethod("*"); // 允许所有HTTP方法
source.registerCorsConfiguration("/**", config); // 对所有路径生效
return new CorsFilter(source);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors().and() // 启用CORS配置
.authorizeRequests().antMatchers("/authenticate").permitAll() // 允许未经验证的访问,这个是登陆的接口,用户通过这个接口获取JWT
.anyRequest().authenticated() // 其他请求需要验证
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 使用无状态会话

http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); // 添加JWT过滤器
}
}

ok到现在基本完成了,再写一个登陆的类就完成了。

@RestController
@CrossOrigin
public class AuthenticationController {
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private MyUserDetailsService userDetailsService;

    @PostMapping("/authenticate")
    @CrossOrigin
    public Result createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
        Result result = new Result();
        try {
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
            );
        } catch (AuthenticationException e) {
            result.setData(MapUtils.map3("loginResult", false, "code", null));
            return result;
        }
        final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
        final String jwt = jwtUtil.generateToken(userDetails.getUsername());
        result.setData(MapUtils.map3("loginResult", true, "code", jwt));
        return result;
    }
}

 

posted on 2024-08-14 02:56  李华超  阅读(30)  评论(0编辑  收藏  举报

导航