java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot Security JWT搭建

SpringBoot Security+JWT简单搭建的实现示例

作者:现在没有牛仔了

本文介绍在Spring Boot 2.6.13项目中集成Security与JWT实现认证鉴权的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

SpringBoot SecuritySpring官方提供的一个安全框架,他的核心功能是对系统用户进行认证和鉴权,也经常在项目中被使用到,本文不介绍其太过深入的内容,只介绍如何实现并完成认证和鉴权的测试。主要分三步来实现:

  1. 配置JWT
  2. 配置Security
  3. 编写测试相关代码

首先创建一个springboot项目,我的版本是2.6.13,依然是java8,整合Security+JWT需要用到的Maven依赖如下:

<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>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

配置JWT

先在yml配置文件中添加jwt相关配置

jwt:
    expiration: 3600000 //token过期时间,1个小时
    tokenHeader: Authorization //token在header中的属性名
    secret: jwt-token-secret  //生成token的密钥

创建jwt工具类,方便实现根据用户信息生成token,以及通过token中获取用户信息

@Component
@Data
public class JwtTokenUtil implements Serializable {
    private static final long serialVersionUID = -3301605591108950415L;
    @Value("${jwt.secret}")
    private  String secret;
    @Value("${jwt.expiration}")
    private Long expiration;

    private Clock clock = DefaultClock.INSTANCE;
    //根据用户信息生成token
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }

    private String doGenerateToken(Map<String, Object> claims, String subject) {
        final Date createdDate = clock.now();
        final Date expirationDate = calculateExpirationDate(createdDate);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    private Date calculateExpirationDate(Date createdDate) {
        return new Date(createdDate.getTime() + expiration);
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        SecurityUserDetails user = (SecurityUserDetails) userDetails;
        final String username = getUsernameFromToken(token);
        return (username.equals(user.getUsername())
                && !isTokenExpired(token)
        );
    }
    //通过token获取用户名username
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

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

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


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

    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

}

配置Security

编写一个存储用户信息的UserDetails的实现类

@Data
public class SysUser {
    private Integer id;
    private String username;
    private String password;
}
@Data
@EqualsAndHashCode
@Accessors(chain = true) //实现链式set方法
public class SecurityUserDetails extends SysUser implements UserDetails {
    //权限列表
    private Collection<? extends GrantedAuthority> authorities;
    public SecurityUserDetails(String userName,Collection<? extends GrantedAuthority> authorities){
        this.setUsername(userName);
        String encode = new BCryptPasswordEncoder().encode("123456");
        this.setPassword(encode);
        this.setAuthorities(authorities);
    }
    /**
     * 下面这些都返回true
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}

提示
因为只是记录一下如何实现security+jwt,所以没有从数据库中读取真实的用户信息,而是直接将用户信息和权限信息写死测试。

重写UserDetailsServiceloadUserByUsername方法实现具体的认证授权逻辑

@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<GrantedAuthority> authorityList = new ArrayList<>();
        authorityList.add(new SimpleGrantedAuthority("ROLE_USER"));
        return new SecurityUserDetails(username,authorityList);
    }
}

提示
这里直接把用户的权限写死,ROLE_USER表示用户拥有USER权限,因为权限都是以ROLE_开头的。

紧接着创建一个用户请求的过滤器,用来拦截用户请求,分析用户有没有该请求的权限

@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
    private final UserDetailsService userDetailsService;
    private final JwtTokenUtil jwtTokenUtil;
    private final String tokenHeader;

    public JwtAuthorizationTokenFilter(@Qualifier("jwtUserDetailsServiceImpl") UserDetailsService userDetailsService,
                                       JwtTokenUtil jwtTokenUtil,
                                       @Value("${jwt.tokenHeader}") String tokenHeader){
        this.userDetailsService = userDetailsService;
        this.jwtTokenUtil = jwtTokenUtil;
        this.tokenHeader = tokenHeader;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String requestHeader = request.getHeader(this.tokenHeader);
        String username = null;
        String authToken = null;
        if(requestHeader != null && requestHeader.startsWith("Bearer ")){
            authToken = requestHeader.substring(7);
            try {
                username = jwtTokenUtil.getUsernameFromToken(authToken);
            }catch (ExpiredJwtException e){
                e.printStackTrace();
            }
        }

        if(username!=null&& SecurityContextHolder.getContext().getAuthentication() == null){
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            if(jwtTokenUtil.validateToken(authToken,userDetails)){
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(request,response);
    }
}

提示
Bearer 必须带空格,第二个if判断就是为了加载到用户的信息,并且在Security上下文中存储用户及用户的权限的信息

实现AuthenticationEntryPoint接口的commence方法,当请求没有携带认证信息或者说认证失败时,使用我们自己编写的处理逻辑。

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}

提示
如果请求没有携带认证信息或者说认证失败时,会返回给客户端401,如果不重写commence方法,默认返回403

接下来编写Security的核心配置类,重写WebSecurityConfigurerAdapter中的configure方法

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    @Autowired
    JwtUserDetailsServiceImpl jwtUserDetailsService;
    @Autowired
    JwtAuthorizationTokenFilter authenticationTokenFilter;
    @Autowired
    @Lazy
    PasswordEncoder passwordEncoder;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .and()
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
                .anyRequest().authenticated() //除上面以外的都拦截
                .and()
                .csrf().disable() //禁用security自带的跨域处理
                //让Security不使用session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

    }

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

    /**
     * 认证逻辑配置
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder);
    }
}

提示
上面的代码中,.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)表示使用自定义的认证失败处理逻辑。并且配置类中,自定义了用户密码的加密方式,configureGlobal方法设置自定义的loadUserByUsername方法实现和校验密码校验的加密方式。

编写测试相关代码

编写一个不需要认证授权就能访问的登录接口/login

@RestController
public class LoginController {
    @Autowired
    @Qualifier("jwtUserDetailsServiceImpl")
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @PostMapping("/login")
    public String login(@RequestBody SysUser sysUser, HttpServletRequest request){
        final UserDetails userDetails = userDetailsService.loadUserByUsername(sysUser.getUsername());
        final String token = jwtTokenUtil.generateToken(userDetails);
        return token;
    }
}

编写一个需要USER权限的接口/sys/testUser

@RestController
@RequestMapping("/sys")
public class SysUserController {
    @PreAuthorize("hasAnyRole('USER')")
    @PostMapping(value = "/testUser")
    public String testNeed() {
        return "hello world";
    }
}

测试

启动SpringBoot项目,对上面的接口进行测试,首先调用/login接口登录并获取token

请求成功并获取到jwt生成的token。紧接着调用需要USER权限的/testUser,请求时要在请求头里面携带token

请求成功!

现在来测试一下失败的情况,不传token直接请求

请求失败,返回401,表示没有认证。再来测试一下如果将@PreAuthorize("hasAnyRole('USER')")中的权限改为Admin,然后用刚刚生成的token去请求

@PreAuthorize("hasAnyRole('Admin')")
@PostMapping(value = "/testUser")
public String testNeed() {
    return "hello world";
}

由于token中包含的授权信息是USER,所以将@PreAuthorize("hasAnyRole('USER')")中的USER改为Admin后,返回了403,表示没有这个权限。

到此这篇关于SpringBoot Security+JWT简单搭建的实现示例的文章就介绍到这了,更多相关SpringBoot Security JWT搭建内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文