springSecurity前后端分离集成jwt


一 前言

大家好,我是知识追寻者,本篇内容是springSecurity第四篇;没有相关基础的同学请学习后再来看这篇内容;文末附源码地址;

二 pom

pom 文件引入的依赖 , security 的启动器支持security 功能;lombok 进行简化开发; fastjson 进行Json处理;

jjwt 进行jwt token 支持;lang3 字符串处理;

 
     
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-security
        
        
            org.projectlombok
            lombok
            1.16.18
            provided
        
        
        
            com.alibaba
            fastjson
            1.2.62
        
        
            io.jsonwebtoken
            jjwt
            0.9.0
        
        
            org.apache.commons
            commons-lang3
            3.4
        

三 认证流程

  • SecurityContextHolder,提供SecurityContext的访问权限。
  • SecurityContext,保存Authentication和可能的特定于请求的安全信息。
  • Authentication,以特定于Spring Security的方式代表校验。
  • GrantedAuthority,以反映授予主体的应用程序范围的权限。
  • UserDetails,提供从应用程序的DAO或其他安全数据源构建Authentication对象所需的信息。
  • UserDetailsService,在基于String的用户名(或证书ID等)中传递时创建UserDetails

上面的意思不难理解, 从数据源中获取 用户信息 组装到 UserDetails, 然后通过UserDetailsService,传递 UserDetails; SecurityContextHolder 存储 整个 用户上下文信息,通过SecurityContext 存储 Authentication, 这样就保证了 springSecurity 持有用户信息;

四 实体

SysUser 实现 UserDetails 用于储存用户信息, 主要是用户名,密码, 和权限;

/**
 * @Author lsc
 * 

*/ @Data public class SysUser implements UserDetails { // 用户名 private String username; // 密码 private String password; // 权限信息 private Set<? extends GrantedAuthority> authorities; @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }

五 token工具类

token 工具类主要用于生产 token, 解析token, 校验token;这边需要注意的是,将 权限 归并到了生成 toekn 的步骤,这样通过 token就可以获取 权限,在权限校验时通过token就可以获取权限信息;缺点就进行授权的之后的token应为未更新会造成权限未同步;

/**
 * @Author lsc
 * 

*/ public class JwtUtil { private static final String CLAIMS_ROLE = "zszxzRoles"; /** * 5天(毫秒) */ private static final long EXPIRATION_TIME = 1000 * 60 * 60 * 5; /** * JWT密码 */ private static final String SECRET = "secret"; /** * 签发JWT */ public static String getToken(String username, String roles) { Map claims = new HashMap<>(8); // 主体 claims.put( CLAIMS_ROLE, roles); return Jwts.builder() .setClaims(claims) .claim("username",username) .setExpiration( new Date( Instant.now().toEpochMilli() + EXPIRATION_TIME ) )// 过期时间 .signWith( SignatureAlgorithm.HS512, SECRET )// 加密 .compact(); } /** * 验证JWT */ public static Boolean validateToken(String token) { return (!isTokenExpired( token )); } /** * 获取token是否过期 */ public static Boolean isTokenExpired(String token) { Date expiration = getExpireTime( token ); return expiration.before( new Date() ); } /** * 根据token获取username */ public static String getUsernameByToken(String token) { String username = (String) parseToken( token ).get("username"); return username; } public static Set getRolseByToken(String token) { String rolse = (String) parseToken( token ).get(CLAIMS_ROLE); String[] strArray = StringUtils.strip(rolse, "[]").split(", "); Set authoritiesSet = new HashSet(); if (strArray.length>0){ Arrays.stream(strArray).forEach(rols-> { GrantedAuthority authority = new SimpleGrantedAuthority(rols); authoritiesSet.add(authority); }); } return authoritiesSet; } /** * 获取token的过期时间 */ public static Date getExpireTime(String token) { Date expiration = parseToken( token ).getExpiration(); return expiration; } /** * 解析JWT */ private static Claims parseToken(String token) { Claims claims = Jwts.parser() .setSigningKey( SECRET ) .parseClaimsJws( token ) .getBody(); return claims; } }

六 UserDetailsService

UserDetailsService 用户查询数据库的数据信息,进行用户数据封装到UserDetails, 在进行用户身份认证的时候会走这边; 这边采用官方提供的PasswordEncoder 进行加密; 其配置方式需要在WebSecurityConfig 中 配置;

/**
 * @Author lsc
 * 

*/ @Component @Slf4j public class SysUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; // 登陆验证时,通过username获取用户的所有权限信息; 正式环境中就是查询用户数据授权 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { log.info("------用户{}身份认证-----",username); // 新建用户 SysUser user = new SysUser(); // 账号 user.setUsername(username); // 密码 user.setPassword(passwordEncoder.encode("123456")); // 设置权限 Set authoritiesSet = new HashSet(); // 注意角色权限需要加 ROLE_前缀,否则报403 GrantedAuthority userPower = new SimpleGrantedAuthority("ROLE_USER"); GrantedAuthority adminPower = new SimpleGrantedAuthority("ROLE_ADMIN"); authoritiesSet.add(userPower); authoritiesSet.add(adminPower); user.setAuthorities(authoritiesSet); return user; } }

七 JWTLoginFilter

JWTLoginFilter 继承 AbstractAuthenticationProcessingFilter 过滤器;理论上继承 UsernamePasswordAuthenticationFilter 也是 可行,毕竟 UsernamePasswordAuthenticationFilter 是 AbstractAuthenticationProcessingFilter 的实现类;

JWTLoginFilter 用于用户登陆认证,其实现如下 三个方法 ;

  • attemptAuthentication 用于 尝试认证,如果认证成功会走 successfulAuthentication 方法;如果认证失败会走 unsuccessfulAuthentication 方法;
  • successfulAuthentication 认证成功后我们需要生成一个token,返回以JSON的形式返回给前端;
  • unsuccessfulAuthentication 认证失败,我们通过异常信息判定,然后返回错误信息给前端;
/**
 * @Author lsc
 * 

登陆认证过滤器

*/ public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter { public JWTLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) { super(new AntPathRequestMatcher(defaultFilterProcessesUrl)); setAuthenticationManager(authenticationManager); } /** * @Author lsc *

登陆认证

* @Param [request, response] * @Return */ public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException { SysUser user = new ObjectMapper().readValue(request.getInputStream(), SysUser.class); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( user.getUsername(), user.getPassword()); return getAuthenticationManager().authenticate(authenticationToken); } /** * @Author lsc *

登陆成功返回token

* @Param [request, res, chain, auth] * @Return */ @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,FilterChain chain, Authentication auth){ SysUser principal = (SysUser)auth.getPrincipal(); String token = JwtUtil.getToken(principal.getUsername(),principal.getAuthorities().toString()); try { //登录成功時,返回json格式进行提示 response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_OK); PrintWriter out = response.getWriter(); ResultPage result = ResultPage.sucess(CodeMsg.SUCESS,token); out.write(new ObjectMapper().writeValueAsString(result)); out.flush(); out.close(); } catch (Exception e1) { e1.printStackTrace(); } } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { String result=""; // 账号过期 if (failed instanceof AccountExpiredException) { result="账号过期"; } // 密码错误 else if (failed instanceof BadCredentialsException) { result="密码错误"; } // 密码过期 else if (failed instanceof CredentialsExpiredException) { result="密码过期"; } // 账号不可用 else if (failed instanceof DisabledException) { result="账号不可用"; } //账号锁定 else if (failed instanceof LockedException) { result="账号锁定"; } // 用户不存在 else if (failed instanceof InternalAuthenticationServiceException) { result="用户不存在"; } // 其他错误 else{ result="未知异常"; } // 处理编码方式 防止中文乱码 response.setContentType("text/json;charset=utf-8"); // 将反馈塞到HttpServletResponse中返回给前台 response.getWriter().write(JSON.toJSONString(result)); } }

八 WebSecurityConfig

WebSecurityConfig 是 springSecurity 的配置相关信息;在配置中,可以进行数据访问权限限制,授权异常处理,账号加密方式等配置;

/**
 * @Author lsc
 * 

*/ @EnableWebSecurity// 开启springSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired DenyHandler denyHandler; @Autowired OutSuccessHandler outSuccessHandler; @Autowired SysUserDetailsService userDetailsService; @Autowired ExpAuthenticationEntryPoint expAuthenticationEntryPoint; /* * * @Author lsc *

授权

* @Param [http] */ protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests()// 授权 .antMatchers("/api/download/**").anonymous()// 匿名用户权限 .antMatchers("/api/**").hasRole("USER")//普通用户权限 .antMatchers("/api/admin/**").hasRole("ADMIN")// 管理员权限 .antMatchers("/login").permitAll() //其他的需要授权后访问 .anyRequest().authenticated() .and()// 异常 .exceptionHandling() .accessDeniedHandler(denyHandler)//授权异常处理 .authenticationEntryPoint(expAuthenticationEntryPoint)// 认证异常处理 .and() .logout() .logoutSuccessHandler(outSuccessHandler) .and() .addFilterBefore(new JWTLoginFilter("/login",authenticationManager()),UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtAuthenticationFilter(authenticationManager()),UsernamePasswordAuthenticationFilter.class) .sessionManagement() // 设置Session的创建策略为:Spring Security不创建HttpSession .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .csrf().disable();// 关闭 csrf 否则post } /* * * @Author lsc *

认证 设置加密方式

* @Param [auth] */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } }

九 Handler

配置中使用到了3个处理类,分别是 denyHandler, outSuccessHandler, expAuthenticationEntryPoint;

其中 denyHandler 当权限进行校验时,如果权限不足就会走这个处理类

/**
 * @Author lsc
 * 

权限不足处理

*/ @Component public class DenyHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { // 设置响应头 httpServletResponse.setContentType("application/json;charset=utf-8"); // 返回值 ResultPage result = ResultPage.error(CodeMsg.PERM_ERROR); httpServletResponse.getWriter().write(JSON.toJSONString(result)); } }

outSuccessHandler 是退出登陆处理类,默认地址 localhost:8080/logout;

/**
 * @Author lsc
 * 

*/ @Component public class OutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { // 设置响应头 httpServletResponse.setContentType("application/json;charset=utf-8"); // 返回值 ResultPage result = ResultPage.sucess(CodeMsg.SUCESS,"退出登陆成功"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); } }

expAuthenticationEntryPoint 负责身份认证通过后异常处理,每个主要身份验证系统都有自己的AuthenticationEntryPoint实现;

/**
 * @Author lsc
 * 

*/ @Component public class ExpAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { // 设置响应头 httpServletResponse.setContentType("application/json;charset=utf-8"); // 返回值 ResultPage result = ResultPage.error(CodeMsg.ACCOUNT_ERROR); httpServletResponse.getWriter().write(JSON.toJSONString(result)); } }

十 Controller

SysUserController 用于 提供权限测试

/**
 * @Author lsc
 * 

*/ @RestController public class SysUserController { @GetMapping("api/admin") @PreAuthorize("hasAuthority('ADMIN')") public String authAdmin() { return "需要ADMIN权限"; } @GetMapping("api/test") @PreAuthorize("hasAuthority('USER')") public String authUser() { return "需要USER权限"; } }

整体项目结构如下

十一 测试

用户登陆 ,返回token

请求接口测试,返回数据

用户退出返回信息;

最后

参考文档

https://blog.csdn.net/Piconjo/article/details/106156383

https://www.jianshu.com/p/8bd4a6e27e7f

https://www.jianshu.com/p/bd882078fac4

https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/

源码地址:欢迎关注公众号:知识追寻者 回复 springSecurity 即可获取啦