spring security详解
框架的作用:
认证(确认用户的身份)
授权(用户有哪些权限,可以调哪些接口)
一、工作原理
https://blog.csdn.net/wu2374633583/article/details/108199205
https://www.cnblogs.com/dalianpai/p/12364330.html
https://www.cnblogs.com/wangstudyblog/p/14793305.html
可以看这三篇讲过滤器的讲的比较详细
FilterChain doFilter方法 进入到过滤器链,多个过滤器封装在FilterChainProxy 中,FilterChainProxy doFilter方法
SecurityContextPersistenceFilter 保存到SecurityContext(安全上下文)中
二、来看一下认证流程
AuthenticationManager(认证管理器)
authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password))-->该方法会去调用UserDetailsServiceImpl.loadUserByUsername
自定义逻辑控制 只需要重写UserDetailsService.loadUserByUsername方法 (查询当前用户名是否在数据库中)
详细的见 https://blog.csdn.net/wwang_dev/article/details/119107355
三、如何自定义配置
之前的过滤器链的博客就有提到(SpringSecurity(八):过滤器链 - 刚刚好。 - 博客园 (cnblogs.com))
WebSecurityConfigurer ---用户扩展用户自定义的配置
你可以通过这个类 配置白名单 配置自定义过滤器 配置异常处理器
四、实现 spring security+jwt
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired JwtLogoutSuccessHandler jwtLogoutSuccessHandler; @Autowired JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired UserDetailServiceImpl userDetailService; @Bean JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager()); return jwtAuthenticationFilter; } /** * 解决 自定义login方法需要注入AuthenticationManager,但是没有,所以在这里注入 AuthenticationManager * * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常 * AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常 * accessDeniedHandler 抛出的异常会被GloableExceptionHandler捕获,因此直接在全局异常处理器捕获把,不在这里写了,spring sccurity官方也不做处理 * 参考:https://github.com/spring-projects/spring-security/issues/6908 * * **/ protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .logout().logoutSuccessHandler(jwtLogoutSuccessHandler) // 禁用session .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 配置白名单 .and().authorizeRequests() .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js" ).permitAll() .antMatchers("/api/email/**","/api/sms/**").permitAll() .antMatchers("/login","/logout").anonymous() //swagger .antMatchers( "/doc.html","/swagger-resources/**","/webjars/**","/*/api-docs","/favicon.ico").anonymous() // .anyRequest().permitAll() .anyRequest().authenticated() // 异常处理器 .and().exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) // 配置自定义的过滤器 .and() .addFilter(jwtAuthenticationFilter()) ; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService); } /** * 强散列哈希加密实现 */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } }
//异常处理器
@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Autowired JwtUtils jwtUtils; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_OK); ServletOutputStream outputStream = response.getOutputStream(); ResultBean result = null; if (request.getHeader(jwtUtils.getHeader())==null){ result = ResultBean.fail(RetCode.UNAUTHEN.code,"令牌不存在,请重新登录"); } else { result = ResultBean.fail(RetCode.UNAUTHEN.code,"认证失败"); } System.out.println("请求错误,进入AuthenticationEntryPoint"); outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8")); outputStream.flush(); outputStream.close(); }
//自定义过滤器
public class JwtAuthenticationFilter extends BasicAuthenticationFilter { @Autowired JwtUtils jwtUtils; @Autowired UserDetailServiceImpl userDetailService; @Autowired SysUserService sysUserService; @Autowired RedisUtil redisUtil; public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String jwt = request.getHeader(jwtUtils.getHeader()); if (StrUtil.isBlankOrUndefined(jwt)) { chain.doFilter(request, response); return; } Claims claim = jwtUtils.getClaimByToken(jwt); if (claim == null) { throw new JwtException("token 异常"); } if (jwtUtils.verifyToken(claim)) { throw new JwtException("登录状态已过期,请重新登录"); } // 通过这个来验证接口用注解标注中的权限 UsernamePasswordAuthenticationToken 是Principal的实现类,参考 /nav UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(claim.get("userId"), null, userDetailService.getUserAuthority(Long.valueOf(claim.get("userId") .toString()))); SecurityContextHolder.getContext().setAuthentication(token); chain.doFilter(request, response); } }
自定义逻辑控制 (生成token之前检查username是否在数据库中)
@Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired SysUserService sysUserService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUserPo sysUserPo =null; sysUserPo = sysUserService.getByUsername(username); if (sysUserPo == null) { sysUserPo =sysUserService.getByPhone(username); } if (sysUserPo == null) { sysUserPo =sysUserService.getByEmail(username); } if (sysUserPo == null){ throw new UsernameNotFoundException("用户名或密码不正确"); } return new LoginUser(sysUserPo.getId(), sysUserPo.getUsername(), sysUserPo.getNickname(),sysUserPo.getPassword(), sysUserPo.getPhone(),sysUserPo.getEmail(),getUserAuthority(sysUserPo.getId())); } /** * 获取用户权限信息(角色、菜单权限) * @param userId * @return */ public List<GrantedAuthority> getUserAuthority(Long userId){ // 角色(ROLE_admin)、菜单操作权限 sys:user:list String authority = sysUserService.getUserAuthorityInfo(userId); // ROLE_admin,ROLE_normal,sys:user:list,.... return AuthorityUtils.commaSeparatedStringToAuthorityList(authority); } }
工具类
package com.prophecy.dental_tech.common.utils; import com.prophecy.dental_tech.common.security.LoginUser; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import lombok.Data; import org.apache.xmlbeans.impl.util.Base64; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; @Data @Component public class JwtUtils { @Autowired private RedisUtil redisUtil; private String secret = "dental_bsz"; private String header = "Authorization"; public byte[] getKey(String secret){ return Base64.encode(secret.getBytes(StandardCharsets.UTF_8)); } // 生成jwt public String generateToken(Long userId, String phone) { Date nowDate = new Date(); Date expireTime = getExpireTime(); return Jwts.builder() .setHeaderParam("typ", "JWT") .claim("userId",userId) .claim("phone",phone) .setSubject(phone) .setIssuedAt(nowDate) .setExpiration(expireTime) .signWith(SignatureAlgorithm.HS512, getKey(secret)) .compact(); } public Date getExpireTime(){ LocalDateTime localDateTime = LocalDate.now().plusDays(1).atStartOfDay(); //下一天的凌晨 Date expireDate = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); return expireDate; } public long getRedisExpireTime(){ return getExpireTime().getTime() - System.currentTimeMillis(); } // 解析jwt public Claims getClaimByToken(String jwt) { try { return Jwts.parser() .setSigningKey(getKey(secret)) .parseClaimsJws(jwt) .getBody(); } catch (Exception e) { return null; } } // jwt是否过期 public boolean verifyToken(Claims claims) { return claims.getExpiration().before(new Date()); } private String getToken(HttpServletRequest request) { return request.getHeader(header); } /** * 获取用户身份信息 * * @return 用户信息 */ public LoginUser getLoginUser(HttpServletRequest request) { // 获取请求携带的令牌 String token = getToken(request); if (StringUtils.isNotEmpty(token)) { try { Claims claims = getClaimByToken(token); // 解析对应的权限以及用户信息 String uuid = (String) claims.get(Constants.JWT_USER_UUID); String redisUserKey = getRedisUserKey(uuid); LoginUser user = (LoginUser)redisUtil.get(redisUserKey); return user; } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } return null; } /** * 获取用户身份信息,现在是手机号 * * @return 用户信息 */ public String getUsername(HttpServletRequest request) { // 获取请求携带的令牌 String token = getToken(request); if (StringUtils.isNotEmpty(token)) { try { Claims claims = getClaimByToken(token); return claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } return null; } /** * 获取用户身份信息,现在是手机号 * * @return 用户信息 */ public String getUserId(HttpServletRequest request) { // 获取请求携带的令牌 String token = getToken(request); if (StringUtils.isNotEmpty(token)) { try { Claims claims = getClaimByToken(token); return claims.get("userId").toString(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } return null; } public String getRedisUserKey(String key){ return Constants.REDIS_LOGIN_USER_KEY +key; } public static void main(String[] args) { String userId="1"; String phone="13246800000"; LocalDateTime localDateTime = LocalDate.now().plusYears(3).atStartOfDay(); //下一天的凌晨 Date expireDate = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); Date nowDate = new Date(); String key = Jwts.builder() .setHeaderParam("typ", "JWT") .claim("userId", userId) .claim("phone", phone) .setSubject(phone) .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS512, "dental_bsz") .compact(); Claims dental_bsz = Jwts.parser() .setSigningKey("dental_bsz") .parseClaimsJws(key) .getBody(); System.out.println(key); // eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOiIxIiwicGhvbmUiOiIxMzI0NjgwMDAwMCIsInN1YiI6IjEzMjQ2ODAwMDAwIiwiaWF0IjoxNjMxMjQ2OTQ3LCJleHAiOjE3MjU4OTc2MDB9.xyIuV5ZVJ_9e1EZrj6JE97IZzU6ckzNcg_vnZoa51mcK2v1g3O5DEId4easvB0O_EVtFUd0AbrMCNz1_qjB9HA } }
生成token接口
1. 校验username password
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
2.再调用 jwtUtils.generateToken(loginUser.getUserId(),loginUser.getPhone()); 返回token
进入接口前的权限校验
@PreAuthorize("hasAuthority('sys:dept:delete')"
@PreAuthorize 权限控制的原理 - 简书 (jianshu.com)