SpringBoot Security+JWT简单搭建

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

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

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

 

xml

代码解读

复制代码

<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

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

    yml

    代码解读

    复制代码

    jwt: expiration: 3600000 //token过期时间,1个小时 tokenHeader: Authorization //token在header中的属性名 secret: jwt-token-secret //生成token的密钥
  2. 创建jwt工具类,方便实现根据用户信息生成token,以及通过token中获取用户信息
     

    java

    代码解读

    复制代码

    @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

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

     

    java

    代码解读

    复制代码

    @Data public class SysUser { private Integer id; private String username; private String password; }

    整理了一份Java面试题。包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

     需要全套面试笔记的【点击此处】即可免费获取

    java

    代码解读

    复制代码

    @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,所以没有从数据库中读取真实的用户信息,而是直接将用户信息和权限信息写死测试。

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

     

    java

    代码解读

    复制代码

    @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_开头的。

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

     

    java

    代码解读

    复制代码

    @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上下文中存储用户及用户的权限的信息

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

     

    java

    代码解读

    复制代码

    @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

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

     

    java

    代码解读

    复制代码

    @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方法实现和校验密码校验的加密方式。

编写测试相关代码

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

    java

    代码解读

    复制代码

    @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; } }
  2. 编写一个需要USER权限的接口/sys/testUser
     

    less

    代码解读

    复制代码

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

测试

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

image.png

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

image.png

 请求成功!

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

image.png

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

 

java

代码解读

复制代码

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

image.png

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

  • 21
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值