spring boot rest 接口集成 spring security(2) – JWT配置

spring boot rest 接口集成 spring security(2) – JWT配置

在教程spring boot rest 接口集成 spring security(1) – 最简配置里介绍了最简集成spring security的过程,本文将继续介绍spring boot项目中集成spring security以及配置jwt的过程。 如果不了解jwt,可以参考5分钟搞懂:JWT(Json Web Token)

项目内容

本文将通过创建一个实际的spring boot项目来演示spring security及jwt的配置过程,项目主要内容:

  • 集成spring security;
  • 配置jwt;
  • 加载用户信息;
  • 实现几个接口,配置访问权限;
  • 最后通过Postman测试接口;

要求

  • JDK1.8或更新版本
  • Eclipse开发环境

如没有开发环境,可参考spring boot 开发环境搭建(Eclipse)

项目创建

创建spring boot项目

打开Eclipse,创建spring boot的spring starter project项目,选择菜单:File > New > Project ...,弹出对话框,选择:Spring Boot > Spring Starter Project,在配置依赖时,勾选web, security,完成项目创建。

项目依赖

要使用jwt,引入jwt jar包

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

项目配置

application.properties配置

## 服务器端口,如果不配置默认是8080端口
server.port=8096 

## jwt配置
#  签名密钥
jwt.secret=my_secret_2019
# jwt有效期(秒)
jwt.expiration=1800

代码实现

项目目录结构如下图,我们添加了几个类,下面将详细介绍。

spring security的配置:SecurityConfig.java

这是spring security的java配置类,几个主要的配置:

  • 用户信息加载配置
  • 权限不足处理配置
  • 权限配置
  • jwt过滤器配置
  • 其他如密码加密,CORS等配置
@Configuration
@EnableWebSecurity // 添加security过滤器
@EnableGlobalMethodSecurity(prePostEnabled = true) // 可以在controller方法上配置权限
public class SecurityConfig extends WebSecurityConfigurerAdapter{
    
  // 加载用户信息
    @Autowired
    private UserDetailsService myUserDetailsService;
    
    // 权限不足错误信息处理,包含认证错误与鉴权错误处理
    @Autowired
    private JwtAuthError myAuthErrorHandler;
    
  // 密码明文加密方式配置
    @Bean
    public PasswordEncoder myEncoder() {
      return new BCryptPasswordEncoder();
    }
    
    // jwt校验过滤器,从http头部Authorization字段读取token并校验
    @Bean
    public JwtAuthFilter myAuthFilter() throws Exception {
        return new JwtAuthFilter();
    }
    
    // 获取AuthenticationManager(认证管理器),可以在其他地方使用
  @Bean(name="authenticationManagerBean")
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }
    
    // 认证用户时用户信息加载配置,注入myUserDetailsService
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
    	auth.userDetailsService(myUserDetailsService);
    }
    
    // 配置http,包含权限配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	http

    	// 由于使用的是JWT,我们这里不需要csrf
    	.csrf().disable()

    	// 基于token,所以不需要session
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

        // 设置myUnauthorizedHandler处理认证失败、鉴权失败
        .exceptionHandling().authenticationEntryPoint(myAuthErrorHandler).accessDeniedHandler(myAuthErrorHandler).and()

        // 设置权限
        .authorizeRequests()

        // 需要登录
        .antMatchers("/hello/hello1").authenticated()

         // 需要角色权限
        .antMatchers("/hello/hello2").hasRole("ADMIN")

        // 除上面外的所有请求全部放开
        .anyRequest().permitAll();

    	// 添加JWT过滤器,JWT过滤器在用户名密码认证过滤器之前
    	http.addFilterBefore(myAuthFilter(), UsernamePasswordAuthenticationFilter.class);

        // 禁用缓存
//    	http.headers().cacheControl();  
    }
    
    // 配置跨源访问(CORS)
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}

 

用户信息及用户信息服务:AuthUser.java,AuthUserService.java

加载用户信息,需要用户信息类及用户信息服务类。AuthUser继承spring的UserDetails,必须重写UserDetails的一些标准接口。注意与实体类User区别。

public class AuthUser implements UserDetails {

  private static final long serialVersionUID = -2336372258701871345L;
  
  //用户实体类
  private User user;
  
  public AuthUser(User user) {
    this.setUser(user);
  }
  
  public static Collection<? extends GrantedAuthority> getAuthoritiesByRole(String role) {
    Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
    
    List<String> roles = Arrays.asList(role.split(","));
    if (roles.contains("user")) {
      authorities.add(new SimpleGrantedAuthority("ROLE_USER")); 
    }
    if (roles.contains("admin")) {
      authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); 
    } 

    return authorities;
  }
  
  // 提供权限信息
  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {

    return getAuthoritiesByRole(getUser().getRole());
  }

  // 提供账号名称
  @Override
  public String getUsername() {
    return getUser().getMobile();
  }

  // 提供密码
  @Override
  public String getPassword() {
    return getUser().getPassword();
  }

  // 账号是否没过期,过期的用户无法认证
  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  // 账号是否没锁住,锁住的用户无法认证
  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  // 密码是否没过期,密码过期的用户无法认证
  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  // 用户是否使能,未使能的用户无法认证
  @Override
  public boolean isEnabled() {
    return true;
  }

  public User getUser() {
    return user;
  }

  public void setUser(User user) {
    this.user = user;
  }

}

AuthUserService继承UserDetailsService,重写了加载用户信息接口:

@Service
public class AuthUserService implements UserDetailsService {

  // 加载用户信息
  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    // 此处应从数据库加载用户信息,为简便起见,直接创建一个用户
    // password的值:2a10EmsokMb6Vkav7m61kY0PtO.ZCLe0h.uJqVAZW7YYBpSUxd/DMkZuG,      // 是明文123456使用BCryptPasswordEncoder加密的值      User user = new User(1l, "abc1", username, "2a10EmsokMb6Vkav7m61kY0PtO.ZCLe0h.uJqVAZW7YYBpSUxd/DMkZuG", "user");
    AuthUser authUser = new AuthUser(user);
    
    return (UserDetails) authUser;
  }
}

 

认证失败、鉴权失败处理:JwtAuthError.java

当认证失败,系统会抛出认证失败异常,可以配置我们自己的认证失败处理类,同样鉴权失败也可以配置我们自己的失败处理类。

JwtAuthError继承AuthenticationEntryPoint(认证失败接口)、AccessDeniedHandler(鉴权失败接口),重写了这2个接口类的失败处理方法,其实JwtAuthError可以分为2个类,我们合二为一了。

@Component
public class JwtAuthError implements AuthenticationEntryPoint, AccessDeniedHandler {

  @SuppressWarnings("unused")
  private static final org.slf4j.Logger log = LoggerFactory.getLogger(JwtAuthError.class);

  // 认证失败处理,返回401 json数据
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
    	
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write("{\"status\":401,\"message\":\"Unauthorized or invalid token\"}");
    	
    }
    
    // 鉴权失败处理,返回403 json数据
  @Override
  public void handle(HttpServletRequest request, HttpServletResponse response,
      AccessDeniedException accessDeniedException) throws IOException, ServletException {
    
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write("{\"status\":403,\"message\":\"Forbidden\"}");
  }
}

 

JWT过滤器

JWT过滤器每次请求应该只执行一次,所以继承OncePerRequestFilter,JWT过滤器的主要行为:

  • 对于每次请求,从http头部Authorization字段中读取jwt
  • 尝试解密jwt,如果正常解出,说明是合法用户
  • 如果是合法用户,设置认证信息,认证通过
@Component
public class JwtAuthFilter extends OncePerRequestFilter {

  private static final org.slf4j.Logger log = LoggerFactory.getLogger(JwtAuthFilter.class);

    @Autowired
    private JwtUtil jwtUtil;

    private String tokenHeader="Authorization";

    private String tokenPrefix="Bearer";

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain) throws ServletException, IOException {
    	
    	// 从http头部读取jwt
        String authHeader = request.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(tokenPrefix)) {
          
          final String authToken = authHeader.substring(tokenPrefix.length() + 1); // The part after "Bearer "
          String username = null, role = null;
          
          // 从jwt中解出账号与角色信息
          try {
          	username = jwtUtil.getUsernameFromToken(authToken);
          	role = jwtUtil.getClaimFromToken(authToken, "role", String.class);
          } catch (Exception e) {
          	log.debug("异常详情", e);
          	log.info("无效token");
          }
          
          // 如果jwt正确解出账号信息,说明是合法用户,设置认证信息,认证通过
          if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
          	
              UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
              		username, null, AuthUser.getAuthoritiesByRole(role));
              
              // 把请求的信息设置到UsernamePasswordAuthenticationToken details对象里面,包括发请求的ip等
              auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
              
              // 设置认证信息
              SecurityContextHolder.getContext().setAuthentication(auth);
            
          }
        }
            
        // 调用下一个过滤器
        chain.doFilter(request, response);
    }
}

 

User实体类(model层)

User实体类对应于数据库中的User表(我们简化了,没有连数据库)

public class User {
    private Long id;

    private String nickname;

    private String mobile;

    private String password;

    private String role;

    public User(Long id, String nickname, String mobile, String password, String role) {
        this.id = id;
        this.nickname = nickname;
        this.mobile = mobile;
        this.password = password;
        this.role = role;
    }

    public User() {
        super();
    }
}

LoginRequest类(model层)

登录请求类,这个类将会接受并校验用户登录时输入的账号密码,关于输入校验,可以参考spring boot输入数据校验(validation)

public class LoginRequest {
  
  @SuppressWarnings("unused")
  private static final org.slf4j.Logger log = LoggerFactory.getLogger(LoginRequest.class);
  
  @NotNull(message="账号必须填")
  @Pattern(regexp = "^[1]([3][0-9]{1}|59|58|88|89)[0-9]{8}$", message="账号请输入11位手机号") // 手机号
  private String account;
  
    @NotNull(message="密码必须填")
    @Size(min=6, max=16, message="密码6~16位")
  private String password;
    
  private boolean rememberMe;
  
  public String getAccount() {
    return account;
  }
  public void setAccount(String account) {
    this.account = account;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
  public boolean isRememberMe() {
    return rememberMe;
  }
  public void setRememberMe(boolean rememberMe) {
    this.rememberMe = rememberMe;
  }
  
}

 

AuthController类(控制层)

AuthController类实现了2个REST API:

  • login – 用户提供账号密码,如果密码正确,返回token,否则返回账号或密码错误提示;
  • refresh 输入一个合法的旧token,返回新token
@RestController
@RequestMapping("/auth")
public class AuthController {
  
    @Autowired
    private AuthService authService;
    
  /**
   * login 
   * @param authRequest
   * @param bindingResult
   * @return ResponseEntity<Result> 
   */
    @RequestMapping(value = "/login", method = RequestMethod.POST, produces="application/json")
    public ResponseEntity<Result> login(@Valid @RequestBody LoginRequest authRequest, BindingResult bindingResult) throws AuthenticationException{
    	
    if(bindingResult.hasErrors()) {			
      Result res = MiscUtil.getValidateError(bindingResult);
      return new ResponseEntity<Result>(res, HttpStatus.UNPROCESSABLE_ENTITY);
    }
    	
        final String token = authService.login(authRequest.getAccount(), authRequest.getPassword());
        
        // Return the token
        Result res = new Result(200, "ok");
        res.putData("token", token);
        return ResponseEntity.ok(res);
    }
    
  /**
   * refresh 
   * @param request
   * @return ResponseEntity<Result> 
   */
    @RequestMapping(value = "/refresh", method = RequestMethod.GET, produces="application/json")
    public ResponseEntity<Result> refresh(HttpServletRequest request, @RequestParam String token) throws AuthenticationException{
    	
    	Result res = new Result(200, "ok");
    	
    	String refreshedToken = authService.refresh(token);
        
        if(refreshedToken == null) {
        	res.setStatus(400);
        	res.setMessage("无效token");
            return new ResponseEntity<Result>(res, HttpStatus.BAD_REQUEST);
        } 
        
        
        res.putData("token", token);
        return ResponseEntity.ok(res);
    }
  
}

 

HelloController类(控制层)

实现了3个REST API:

  • hello1
  • hello2
  • hello3

用于测试权限配置

@RestController
@RequestMapping("/hello")
public class HelloController {
  
  @RequestMapping(value="/hello1", method=RequestMethod.GET)
    public String hello1() {
          
        return "Hello1!";
    }
  
  @RequestMapping(value="/hello2", method=RequestMethod.GET)
    public String hello2() {
          
        return "Hello2!";
    }
  
  @RequestMapping(value="/hello3", method=RequestMethod.GET)
    public String hello3() {
          
        return "Hello3!";
    }
}

AuthService接口与AuthServiceImpl实现类(服务层)

AuthService提供对AuthController的服务

AuthService.java

public interface AuthService {
    User register(User userToAdd);
    String login(String username, String password);
    String refresh(String oldToken);
}

 

AuthServiceImpl.java

@Service
public class AuthServiceImpl implements AuthService {
  
  private static final org.slf4j.Logger log = LoggerFactory.getLogger(AuthServiceImpl.class);

    private AuthenticationManager authenticationManager;
    private UserDetailsService userDetailsService;
    private JwtUtil jwtUtil;

    @Autowired
    public AuthServiceImpl(
            AuthenticationManager authenticationManager,
            UserDetailsService userDetailsService,
            JwtUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
        this.userDetailsService = userDetailsService;
        this.jwtUtil = jwtUtil;
    }

    @Override
    public User register(User userToAdd) {
    	// TODO: 保存user到数据库
        return null;
    }

    @Override
    public String login(String username, String password) {
    	// 认证用户,认证失败抛出异常,由JwtAuthError的commence类返回401
        UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
        final Authentication authentication = authenticationManager.authenticate(upToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        // 如果认证通过,返回jwt
    	final AuthUser userDetails = (AuthUser) userDetailsService.loadUserByUsername(username);
        final String token = jwtUtil.generateToken(userDetails.getUser());
        return token;
    }

    @Override
    public String refresh(String oldToken) {
        String newToken = null;
        
        try {
        	newToken = jwtUtil.refreshToken(oldToken);
        } catch (Exception e) {
        	log.debug("异常详情", e);
        	log.info("无效token");
        }
    return newToken;
    }
}

 

其他

剩下的一些类

  • Result.java 结果封装类
  • MiscUtil.java 辅助类
  • JwtUtil.java jwt处理类,加密解密等操作

运行

Eclipse左侧,在项目根目录上点击鼠标右键弹出菜单,选择:run as -> spring boot app运行程序。 打开Postman访问接口,运行结果如下:

访问/hello/hello1接口,需要登录访问,没有带上token,返回401

登录获取token

再次访问需要登录访问的/hello/hello1接口,带上token,可以看到访问成功

访问需要admin权限的/hello/hello2接口,虽然带上token,但权限不足,可以看到返回403

总结

完成。由于作者水平有限,错漏缺点在所难免,欢迎批评指正。

完整代码

github


评论

nnn
MiscUtil.java 辅助类 JwtUtil.java jwt处理类,加密解密等操作 这两个类不放出来吗?
回复 · 4年前

吴吃辣
看文章最后的源代码,github里有。
回复 · 4年前