SpringBoot集合SpringSecurity流程及代碼詳解和導圖詳解


  最近入手做Java項目,使用SpringBoot和安全框架SpringSecurity,之前也總結了很多問題解決的博客,可以查看之前博客,這篇是決定把整個流程及大致代碼記錄一下,當然我只貼關鍵代碼流程哦。

一、流程和代碼詳解

1、首先需要導入核心依賴

<!-- spring security-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2、然后看一下代碼結構

  最主要的就是 security 包下的內容咯。

3、WebSecurityConfig類

  最重要的就是 WebSecurityConfig 類咯,這個類得繼承至 WebSecurityConfigurerAdapter 類,並且得加上 @EnableWebSecurity 注解,即啟用web安全。

  WebSecurityConfig類使用了@EnableWebSecurity注解 ,以啟用SpringSecurity的Web安全支持,並提供Spring MVC集成。它還擴展了WebSecurityConfigurerAdapter,並覆蓋了一些方法來設置Web安全配置的一些細節。

  比如可以覆寫configure(HttpSecurity)方法定義了哪些URL路徑應該被保護,哪些不應該。

  項目實例代碼:

@Configuration @EnableWebSecurity // 開啟web安全支持 public class WebSecurityConfig  extends WebSecurityConfigurerAdapter { ...... @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable().cors().and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // user
            .authorizeRequests().antMatchers(HttpMethod.GET,"/user/code").permitAll() .antMatchers(HttpMethod.POST, "/user/resetPassword", "/user/alipayNotify").permitAll() .anyRequest().authenticated() // 以上配置的路徑不需要認證,anyRequest其他任何都需要認證 .and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint()) // 異常時走這個自定義的提示 .and() .addFilterBefore( // 添加自定義登錄攔截過濾器 new JWTLoginFilter( new AntPathRequestMatcher("/login", HttpMethod.POST.name()), authenticationManager() ), UsernamePasswordAuthenticationFilter.class ) .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
       // 添加JWT權限認證攔截器,用於將每個接口攔截進行token驗證,將token里的信息拿取用戶並放入安全上下文 }
  ......
}

4、JWTLoginFilter 自定義登錄過濾器

  JWTLoginFilter主要是用來處理自定義登錄的業務邏輯處理。我們的登錄是第一次驗證碼登錄,之后用戶也可以設置密碼用密碼登錄。

// 需要繼承AbstractAuthenticationProcessingFilter類,然后覆寫下面這3個方法,IDE會自動給提示 public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter { public JWTLoginFilter(RequestMatcher requestMatcher, AuthenticationManager authManager) { super(requestMatcher); setAuthenticationManager(authManager); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException { ......// 登錄是否含手機號,從請求流轉換為User實例
        User voUser = new ObjectMapper().readValue(req.getInputStream(), User.class); if (voUser == null || voUser.getPhoneNum() == null) { throw new AuthenticationServiceException("請輸入手機號"); }
     // 通過 反射 獲取到UserService,然后從數據庫里取到當前請求用戶數據 UserService userService
= SpringUtil.getBean(UserService.class); User dbUser = userService.getUserByPhoneNum(voUser.getPhoneNum()); // 如果是密碼登錄,需要校驗密碼 if (StringUtils.isBlank(voUser.getCode())) { if (dbUser == null) { throw new AuthenticationServiceException("用戶不存在,請使用驗證碼注冊"); } ...... voUser.setUserId(dbUser.getUserId()); //添加用戶id,登錄方式到session,方便后續驗證 req.setAttribute("userId", dbUser.getUserId()); return getAuthenticationManager().authenticate( new UsernamePasswordAuthenticationToken( voUser.getUserId(), voUser.getPassword() ) ); } else { // 驗證碼登錄:需要校驗驗證碼 ......if (!userService.verifySmsCode(voUser.getPhoneNum(), voUser.getCode())) { throw new AuthenticationServiceException("短信驗證碼錯誤"); } if(dbUser == null) { userService.saveUserPhone(voUser); dbUser = userService.getUserByPhoneNum(voUser.getPhoneNum()); } req.setAttribute("loginType", 1); req.setAttribute("userId", dbUser.getUserId());
       // 由於可以驗證碼登錄,用戶在庫里也可能沒有密碼,所以無法用上面 security 自帶校驗方式
       // 自定義校驗方式,只要驗證碼校驗成功,那么就通過用戶Id,去
TokenAuthenticationService里生成 token TokenAuthenticationService.addAuthentication(res, String.valueOf(dbUser.getUserId())); OperationInfo info = OperationInfo.success("登錄成功"); HttpResponseUtil.setResponseMessage(res, info); return null; } }
   // 這個是需要覆寫 security 自帶的校驗成功方法,也就是登錄校驗成功之后,去
TokenAuthenticationService里生成 token @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException { TokenAuthenticationService.addAuthentication(res, auth.getName()); OperationInfo info = OperationInfo.success("登錄成功"); HttpResponseUtil.setResponseMessage(res, info); }    // 這個是需要覆寫 security 自帶的校驗失敗方法,也就是登錄失敗之后,給出提示,這個OperationInfo也就是我們項目定義的返回操作類 @Override protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res, AuthenticationException failed) throws IOException { OperationInfo info; if (failed instanceof BadCredentialsException) { info = OperationInfo.failure("用戶密碼錯誤"); } else { info = OperationInfo.failure(failed.getMessage()); } HttpResponseUtil.setResponseMessage(res, info); } }

5、TokenAuthenticationService 生成及解析token

  接上面我們都用到了 TokenAuthenticationService 去生成 token,那么我們看一下這個Service的主要作用,其實就是生成 JWT token,和解析JWT token。

@Slf4j @Component public class TokenAuthenticationService { private static final long EXPIRATIONTIME = 604_800_000; // 7 days
    private static String SECRET; // 簽名 private static final String TOKEN_PREFIX = "Bearer"; private static final String HEADER_STRING = "Authorization"; @Resource private EnvService envService; // 自定義工具類,用於判斷環境從而生成不同的 secret
@PostConstruct // 這個注解可以了解下,加載servlet的時候運行
public void setSecret() { if (envService.isProd()) { SECRET = "TokenSecret"; } else { SECRET = "TestTokenSecret"; } } public static Authentication getAuthentication(HttpServletRequest req, HttpServletResponse res) { //如果header沒有auth頭,從cookie獲取token String token = req.getHeader(HEADER_STRING); Cookie[] cookies = req.getCookies(); if (cookies != null && cookies.length > 0) { for (Cookie cookie : cookies) { if (Objects.equals(cookie.getName(), "token")) { try { token = URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { log.error(LogUtil.getStack(e)); } } } } if (StringUtils.isNotBlank(token) && token.length() != 32) { // parse the token. Claims body = Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token.replace(TOKEN_PREFIX, "")) .getBody(); String username = body.getSubject(); String role = (String) body.get("roleName"); long exp = body.getExpiration().getTime(); long now = System.currentTimeMillis(); if (username != null) { if (exp - 864_00_000 < now) { //1 day left refresh 如果過期時間只在1天內,那么就重新生成一個新token給用戶端,避免過期 addAuthentication(res, username, role); } return new UsernamePasswordAuthenticationToken(username, null, RoleConfig.getAuthoritiesWithoutPrefix(role)); } } return null; } public static void addAuthentication(HttpServletResponse res, String username, String role) { String JWT = Jwts.builder() .setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME)) .claim("roleName", role) .signWith(SignatureAlgorithm.HS512, SECRET) .compact(); res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT); } public static void addAuthentication(HttpServletResponse res, String username) { Integer userId = Integer.valueOf(username); UserService userService = SpringUtil.getBean(UserService.class); User user = userService.getUserById(userId); String role = RoleConfig.ROLE_PREFIX + user.getRoleName(); addAuthentication(res, username, role); } }

  然后看一下 EnvService ,這個就是用來判斷環境的

@AllArgsConstructor @Service public class EnvService { private Environment environment; public boolean isProd() { for (String activeProfile : environment.getActiveProfiles()) {   return StringUtils.contains(activeProfile, "prod"); } return false; } }

  然后再看下 RoleConfig,這個就是將從 token 中拿到的角色、權限等放到列表中,分別是帶前綴和不帶前綴。因為 security 里默認得帶前綴 "Role_",而返回給用戶端的就不需要這個前綴了。

  並且我們在這個類上開啟了 security 的方法級管控。

@Configuration @EnableGlobalMethodSecurity( // 開啟了Security的方法級管控 prePostEnabled = true, securedEnabled = true, jsr250Enabled = true ) public class RoleConfig extends GlobalMethodSecurityConfiguration { public static final String ROLE_PREFIX = "ROLE_"; public static Collection<GrantedAuthority> getAuthorities(String roleName, List<String> permissions) { List<GrantedAuthority> authList = new ArrayList<>(); authList.add(new SimpleGrantedAuthority(ROLE_PREFIX + roleName)); if (permissions != null) { authList.addAll( permissions.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()) ); } return authList; } public static Collection<GrantedAuthority> getAuthoritiesWithoutPrefix(String roleName, List<String> permissions) { List<GrantedAuthority> authList = new ArrayList<>(); authList.add(new SimpleGrantedAuthority(roleName)); if (permissions != null) { authList.addAll( permissions.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()) ); } return authList; } }

6、自定義UserDetailsService

  關於自定義登錄攔截還有一個最重要的就是:自定義UserDetailsService

@Configuration @EnableWebSecurity public class WebSecurityConfig  extends WebSecurityConfigurerAdapter { @Autowired private ExamUserDetailsService examUserDetailsService;  @Bean public AuthenticationEntryPoint authenticationEntryPoint() { return new EmcsAuthenticationEntryPoint(); } @Override protected void configure(HttpSecurity http) throws Exception { ...... } @Override // 這個configure就是用來定義密碼校驗的,可以使用security自帶的BCryptPasswordEncoder,也可以使用自定義的 protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(examUserDetailsService).passwordEncoder(new BCryptPasswordEncoder()); } }

  這個自定義的UserService里最重要的就是要覆寫 loadUserByUsername 方法,這個是源碼里面看來的

@Service public class ExamUserDetailsService implements UserDetailsService { @Autowired private UserService userService;    // 覆寫 loadUserByUsername 方法,從數據庫里取出用戶數據,這個參數username,其實就是userId
   // 下面這個是 security 的 User,他的角色默認是帶 Role_ 前綴的,所以 RoleConfig.getAuthorities 的角色就是需要帶 Role_ 前綴
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Integer userId = Integer.valueOf(username); User user = userService.getUserById(userId); if (user == null) { throw new UsernameNotFoundException("用戶不存在"); } return new org.springframework.security.core.userdetails.User( username, user.getPassword(), true, true, true, true, RoleConfig.getAuthorities(user.getRoleName()) ); } }

  這樣自定義登錄攔截的業務就差不多了。

7、JWTAuthenticationFilter 攔截器

  這個攔截器是請求過濾,所有的請求都會走這個攔截。這個攔截器的主要作用是:進行token驗證,並把解析的用戶信息放入安全上下文。

public class JWTAuthenticationFilter extends GenericFilterBean { @Override // 覆寫 doFilter 方法,走我們的自定義業務邏輯 public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { // 可以取到用戶信息的(有正確token)將用戶信息放入上下文
        try { Authentication auth = TokenAuthenticationService.getAuthentication((HttpServletRequest) request, (HttpServletResponse) response); SecurityContextHolder.getContext().setAuthentication(auth); } catch (MalformedJwtException | SignatureException | ExpiredJwtException | UnsupportedJwtException e) {
       // 需要捕獲一下上面4個異常,如果異常就將上下文設為null,類似於游客訪問 SecurityContextHolder.getContext().setAuthentication(
null); } finally{ filterChain.doFilter(request, response); } } }

8、UserUtils 安全上下文信息工具類

...... // 主要是這三個重要的類 import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; public class UserUtils {public static User getCurrentUser() { Integer userId = getCurrentUserId(); if (userId == null) { return null; } UserService userService = SpringUtil.getBean(UserService.class); return userService.getUserById(userId); }
  
public static Integer getCurrentUserId() { Authentication auth = getAuth(); // 從安全上下文拿到憑證,取到 userId if (auth == null) { return null; } String userId = (String) auth.getPrincipal(); if (userId == null || StringUtils.equalsIgnoreCase("anonymousUser", userId)) { return null; } return Integer.valueOf(userId); }
   // 從安全上下文拿到角色
public static String getRole() { GrantedAuthority[] authorities = getAuth().getAuthorities().toArray(new GrantedAuthority[1]); if (authorities.length > 0) { for (GrantedAuthority authority : authorities) { String authorityName = authority.getAuthority(); if (authorityName.startsWith(RoleConfig.ROLE_PREFIX)) { return authorityName.substring(5); } } } return null; }
   // 從 安全上下文獲取到 憑證
private static Authentication getAuth() { return SecurityContextHolder.getContext().getAuthentication(); } }

  主要是從安全上下文拿到 憑證、拿到userId之類的信息,然后通過這個信息,就可以查數據庫拿到很多數據。

9、AuthenticationEntryPoint:自定義未登錄的返回狀態

  還有一個需要注意的是 security 登錄不成功默認返回的是它自帶的 403 之類的信息,我們如果需要自定義成我們想要的結構的話,就需要實現AuthenticationEntryPoint,然后覆寫 commence 方法。

@Configuration @EnableWebSecurity public class WebSecurityConfig  extends WebSecurityConfigurerAdapter {  @Bean public AuthenticationEntryPoint authenticationEntryPoint() { return new EmcsAuthenticationEntryPoint(); }
   ......
}

  先在 WebSecurityConfig 里注冊AuthenticationEntryPoint,然后使用我們自定義的EmcsAuthenticationEntryPoint

public class EmcsAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException { response.setStatus(HttpStatus.UNAUTHORIZED.value()); OperationInfo info = OperationInfo.failure("請登錄后操作"); HttpResponseUtil.setResponseMessage(response, info); } }

二、導圖流程解析

  這個導圖就是上面這個流程介紹,其中單點登錄,是我們有2個平台,一個墨天輪平台,一個內部知識庫平台,登錄放在墨天輪平台,所以內部知識庫平台只需要一個過濾器 JWTAuthenticationFilter 即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM