SpringBoot3+SpringSecurity整合
Security導包:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
模擬Redis儲存登入資訊:
public class CacheEntity implements Serializable { private Object value; /** * 儲存的時間戳 */ private long gmtModify; /** * 過期時間 */ private int expire; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } public long getGmtModify() { return gmtModify; } public void setGmtModify(long gmtModify) { this.gmtModify = gmtModify; } public int getExpire() { return expire; } public void setExpire(int expire) { this.expire = expire; } public CacheEntity(Object value, long gmtModify, int expire) { this.value = value; this.gmtModify = gmtModify; this.expire = expire; } }
@Slf4j public class LocalCache { /** * 預設的快取容量 */ private static final int DEFAULT_CAPACITY = 512; /** * 最大容量 */ private static final int MAX_CAPACITY = 100000; /** * 重新整理快取的頻率 */ private static final int MONITOR_DURATION = 2; // 啟動監控執行緒 static { new Thread(new TimeoutTimerThread()).start(); } // 內部類方式實現單例 private static class LocalCacheInstance { private static final LocalCache INSTANCE = new LocalCache(); } public static LocalCache getInstance() { return LocalCacheInstance.INSTANCE; } private LocalCache() { } /** * 使用預設容量建立一個Map */ private static Map<String, CacheEntity> cache = new ConcurrentHashMap<>(DEFAULT_CAPACITY); /** * 將key-value儲存到本地快取並制定該快取的過期時間 * * @param key * @param value * @param expireTime 過期時間,如果是-1 則表示永不過期 * @param <T> * @return */ public <T> boolean putValue(String key, T value, int expireTime) { return putCloneValue(key, value, expireTime); } /** * 將值透過序列化clone 處理後儲存到快取中,可以解決值引用的問題 * * @param key * @param value * @param expireTime * @param <T> * @return */ private <T> boolean putCloneValue(String key, T value, int expireTime) { try { if (cache.size() >= MAX_CAPACITY) { return false; } // 序列化賦值 CacheEntity entityClone = clone(new CacheEntity(value, System.nanoTime(), expireTime)); cache.put(key, entityClone); return true; } catch (Exception e) { log.error("新增快取失敗:{}", e.getMessage()); } return false; } /** * 序列化 克隆處理 * * @param object * @param <E> * @return */ private <E extends Serializable> E clone(E object) { E cloneObject = null; try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(object); oos.close(); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); cloneObject = (E) ois.readObject(); ois.close(); } catch (Exception e) { log.error("快取序列化失敗:{}", e.getMessage()); } return cloneObject; } /** * 從本地快取中獲取key對應的值,如果該值不存則則返回null * * @param key * @return */ public Object getValue(String key) { if (CollectionUtils.isEmpty(cache)) { return null; } CacheEntity cacheEntity = cache.get(key); if (ObjectUtils.isEmpty(cacheEntity)) { return null; } return cacheEntity.getValue(); } public void remove(String key) { if (CollectionUtils.isEmpty(cache)) { return; } CacheEntity cacheEntity = cache.get(key); if (ObjectUtils.isEmpty(cacheEntity)) { return; } cache.remove(key); } /** * 清空所有 */ public void clear() { cache.clear(); } /** * 過期處理執行緒 */ static class TimeoutTimerThread implements Runnable { @Override public void run() { while (true) { try { TimeUnit.SECONDS.sleep(MONITOR_DURATION); checkTime(); } catch (Exception e) { log.error("過期快取清理失敗:{}", e.getMessage()); } } } /** * 過期快取的具體處理方法 * * @throws Exception */ private void checkTime() throws Exception { // 開始處理過期 for (String key : cache.keySet()) { CacheEntity tce = cache.get(key); long timoutTime = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - tce.getGmtModify()); // 過期時間 : timoutTime if (tce.getExpire() > timoutTime) { continue; } log.info(" 清除過期快取 :" + key); //清除過期快取和刪除對應的快取佇列 cache.remove(key); } } } }
許可權列舉:
// 許可權值是將二進位制與十進位制相互轉換來判斷的 public enum PermissionEnum { GET_DEPARTMENT(1, "單位獲取", "ROLE_GET_DEPARTMENT", 0x0000000000000001L), INSERT_DEPARTMENT(2, "單位增加", "ROLE_INSERT_DEPARTMENT", 0x0000000000000002L), UPDATE_DEPARTMENT(3, "單位修改", "ROLE_UPDATE_DEPARTMENT", 0x0000000000000004L), DELETE_DEPARTMENT(4, "單位刪除", "ROLE_DELETE_DEPARTMENT", 0x0000000000000008L), ; private int id; private String permissions; private String permissionNames; private Long value; PermissionEnum(int id, String permissions, String permissionNames, Long value) { this.id = id; this.permissions = permissions; this.permissionNames = permissionNames; this.value = value; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getPermissions() { return permissions; } public void setPermissions(String permissions) { this.permissions = permissions; } public String getPermissionNames() { return permissionNames; } public void setPermissionNames(String permissionNames) { this.permissionNames = permissionNames; } public Long getValue() { return value; } public void setValue(Long value) { this.value = value; } public static List<GrantedAuthority> fromCode(Long code) { List<GrantedAuthority> list = new ArrayList<>(); PermissionEnum[] codes = PermissionEnum.values(); for (PermissionEnum state : codes) { if ((state.getValue() & code) > 0) { list.add(new SimpleGrantedAuthority(state.getPermissionNames())); } } return list; } public static List<PermissionEnum> getAuthList(Long code) { List<PermissionEnum> list = new ArrayList<>(); PermissionEnum[] codes = PermissionEnum.values(); for (PermissionEnum state : codes) { if ((state.getValue() & code) > 0) { list.add(state); } } return list; } // 獲取許可權值 public static Long getPermissionCode(Integer[] auths) { Long code = 0x0000000000000000L; PermissionEnum[] codes = PermissionEnum.values(); for (Integer auth : auths) { for (PermissionEnum permissionCode : codes) { if (auth.equals(permissionCode.getId())) { code += permissionCode.getValue(); break; } } } return code; } // 獲取許可權陣列 public static String[] getAuths(Long code) { List<String> lists = new ArrayList<>(); PermissionEnum[] codes = PermissionEnum.values(); for (PermissionEnum state : codes) { if ((state.getValue() & code) > 0) { lists.add(state.getPermissions()); } } return lists.toArray(new String[lists.size()]); } // 獲取許可權值 public static Long getPermissionCode(String[] auths) { Long code = 0x0000000000000000L; PermissionEnum[] codes = PermissionEnum.values(); for (String auth : auths) { for (PermissionEnum permissionCode : codes) { if (auth.equals(permissionCode.getPermissions())) { code += permissionCode.getValue(); break; } } } return code; } }
User實體類:
@Data @Accessors(chain = true) public class Users implements Serializable { private Long userID; private String userName; private String userPassword; private String userPhone; private String userAddress; private Integer userAllowErrCount; private Integer userErrCount; private Date userLastErrTime; private Long userRoleID; private Roles roles; private Long userDepID; private Department department; private boolean userEnable; }
許可權反序列化:
public class CustomAuthorityDeserializer extends JsonDeserializer { @Override public Object deserialize( JsonParser p, DeserializationContext deserializationContext ) throws IOException, JacksonException { ObjectMapper mapper = (ObjectMapper) p.getCodec(); JsonNode jsonNode = mapper.readTree(p); LinkedList<GrantedAuthority> grantedAuthorities = new LinkedList<>(); Iterator<JsonNode> elements = jsonNode.elements(); while (elements.hasNext()) { JsonNode next = elements.next(); JsonNode authority = next.get("authority"); //將得到的值放入連結串列 最終返回該連結串列 grantedAuthorities.add(new SimpleGrantedAuthority(authority.asText())); } return grantedAuthorities; } }
使用者詳情類:
@JsonIgnoreProperties({"enabled", "accountNonExpired", "accountNonLocked", "credentialsNonExpired", "username", "password"}) public class MyUserDetail extends Users implements UserDetails, Serializable { List<? extends GrantedAuthority> authorities; public MyUserDetail() { } public MyUserDetail(Users users, List<? extends GrantedAuthority> authList) { this.setUserID(users.getUserID()); this.setUserName(users.getUserName()); this.setUserPassword(users.getUserPassword()); this.setUserDepID(users.getUserDepID()); this.setUserRoleID(users.getUserRoleID()); this.authorities = authList; } @Override @JsonDeserialize(using = CustomAuthorityDeserializer.class) public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return this.getUserPassword(); } @Override public String getUsername() { return this.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
使用者詳情實現類:
@Component public class MyUserDetailServiceImpl implements UserDetailsService { // 運算元據庫,根據使用者名稱稱查詢使用者資訊 private final UserMapper userMapper; public MyUserDetailServiceImpl(UserMapper userMapper) { this.userMapper = userMapper; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Users users = userMapper.getUserByName(username); Optional.ofNullable(users).orElseThrow(() -> { // 自定義的異常返回類和列舉 throw new CommonException(YIXGResultEnum.USER_NOT_EXIST.getCode(), YIXGResultEnum.USER_NOT_EXIST.getMessage()); }); if (ObjectUtils.isEmpty(users.getUserRoleID())) { throw new CommonException(YIXGResultEnum.USER_ROLE_NOT_EXIST.getCode(), YIXGResultEnum.USER_ROLE_NOT_EXIST.getMessage()); } List<GrantedAuthority> authorityList = PermissionEnum.fromCode(users.getRoles().getRolePermission()); return new MyUserDetail(users, authorityList); } }
攔截未登入請求:
/** * 使用者發起未登入的請求會被AuthorizationFilter攔截,並丟擲AccessDeniedException異常。異常被AuthenticationEntryPoint * 處理,預設會觸發重定向到登入頁。Spring Security開放了配置,允許我們自定義AuthenticationEntryPoint。 * 那麼我們就透過自定義AuthenticationEntryPoint來取消重定向行為,將介面改為返回JSON資訊。 */ public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence( HttpServletRequest request, HttpServletResponse response, AuthenticationException authException ) throws IOException, ServletException { CommonResult commonResult = new CommonResult(); ObjectMapper objectMapper = new ObjectMapper(); commonResult.setCode(YIXGResultEnum.LOGIN_INVALID.getCode()) .setMessage(YIXGResultEnum.LOGIN_INVALID.getMessage()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(commonResult)); response.getWriter().flush(); response.getWriter().close(); } }
攔截沒許可權的請求:
public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle( HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException ) throws IOException, ServletException { ObjectMapper objectMapper = new ObjectMapper(); CommonResult commonResult = new CommonResult(); commonResult.setCode(YIXGResultEnum.NO_PERMISSION.getCode()) .setMessage(YIXGResultEnum.NO_PERMISSION.getMessage()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(commonResult)); response.getWriter().flush(); response.getWriter().close(); } }
自定義攔截器,驗證token資訊:
public class MyAuthenticationTokenFilter extends OncePerRequestFilter { @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { // 從header中獲取驗證資訊 String authHeader = request.getHeader(GlobalUtil.AUTHORIZATION); if (ObjectUtils.isEmpty(authHeader)) { filterChain.doFilter(request, response); return; } this.doParse(request, response, filterChain, authHeader); } private void doParse( HttpServletRequest request, HttpServletResponse response, FilterChain chain, String authHeader ) throws ServletException, IOException { ObjectMapper objectMapper = new ObjectMapper(); // 如果認證碼 以規定值開頭 if (authHeader.startsWith(GlobalUtil.GRANT_TYPE)) { // 提取token值 String token = authHeader.substring(GlobalUtil.GRANT_TYPE.length()); if (ObjectUtils.isEmpty(token)) { chain.doFilter(request, response); return; } // 透過token值從快取中取使用者資訊 String userJson = (String) LocalCache.getInstance().getValue(token); // 轉換JSON物件 //JSONObject userJsonObject = JSON.parseObject(userJson); // 判斷是否空值 if (ObjectUtils.isEmpty(userJson)) { // throw new CommonException(YIXGResultEnum.INVALID_TOKEN.getCode(), // YIXGResultEnum.INVALID_TOKEN.getMsg()); chain.doFilter(request, response); return; } // 轉換MyUserDetail物件 MyUserDetail user = objectMapper.readValue(userJson, MyUserDetail.class); //MyUserDetail user = JSON.toJavaObject(userJsonObject, MyUserDetail.class); // MyUserDetail user = JSONObject.toJavaObject(userJsonObject, MyUserDetail.class); // 轉換 UP 物件放到上下文中 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( user, user.getPassword(), user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } chain.doFilter(request, response); } }
密碼加密:
public class MyPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { return MD5Util.md5((String) rawPassword); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equalsIgnoreCase(MD5Util.md5((String) rawPassword)); } }
登入成功:
@Component public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private final ObjectMapper objectMapper; private final LogService logService; private final UserService userService; public MyAuthenticationSuccessHandler( ObjectMapper objectMapper, LogService logService, UserService userService ) { this.objectMapper = objectMapper; this.logService = logService; this.userService = userService; } @Override public void onAuthenticationSuccess( HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication ) throws IOException, ServletException { AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication); } @Override public void onAuthenticationSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication ) throws IOException, ServletException { MyUserDetail user = (MyUserDetail) authentication.getPrincipal(); // 獲取隨機token 並存到Redis中 String token = UUID.randomUUID().toString().replaceAll("-", ""); LocalCache.getInstance().putValue(token, objectMapper.writeValueAsString(user), 60 * 60); UserVO userVO = new UserVO(); userVO.setUserName(user.getUserName()) .setUserErrCount("0") .setUserLastErrTime(null); userService.updateUserErrCount(userVO); LogVO logVO = new LogVO(); logVO.setLogOperateUser(user.getUserName()) .setLogContent("登入成功") .setLogType("登入日誌"); logService.addLog(logVO); CommonResult commonResult = new CommonResult(); commonResult.setCode(YIXGResultEnum.OPERATE_SUCCESS.getCode()) .setMessage(YIXGResultEnum.OPERATE_SUCCESS.getMessage()) .setToken(token) .setCurrentUser(user.getUserName()) .setCurrentUserId(user.getUserID()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(commonResult)); response.getWriter().flush(); response.getWriter().close(); } }
登入失敗:
@Component @Slf4j public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { private final ObjectMapper objectMapper; private final UserService userService; public MyAuthenticationFailureHandler(ObjectMapper objectMapper, UserService userService) { this.objectMapper = objectMapper; this.userService = userService; } @Override public void onAuthenticationFailure( HttpServletRequest request, HttpServletResponse response, AuthenticationException exception ) throws IOException, ServletException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); CommonResult result = userService.getUserByUserName( new UserVO().setUserName(request.getParameter("username"))); Users users = (Users) result.getObjectData(); if (Objects.equals(result.getCode(), YIXGResultEnum.OPERATE_SUCCESS.getCode())) { UserVO userVO = new UserVO(); userVO.setUserName(users.getUserName()) .setUserErrCount(String.valueOf((users.getUserErrCount() + 1))) .setUserLastErrTime(sdf.format(new Date())); userService.updateUserErrCount(userVO); } CommonResult commonResult = new CommonResult(); commonResult.setCode(YIXGResultEnum.PASSWORD_OR_USERNAME_ERROR.getCode()) .setMessage(YIXGResultEnum.PASSWORD_OR_USERNAME_ERROR.getMessage()); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(commonResult)); response.getWriter().flush(); response.getWriter().close(); } }
登出成功:
@Component public class MyLogoutSuccessHandler implements LogoutSuccessHandler { private final ObjectMapper objectMapper; private final LogService logService; public MyLogoutSuccessHandler(ObjectMapper objectMapper, LogService logService) { this.objectMapper = objectMapper; this.logService = logService; } @Override public void onLogoutSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication ) throws IOException, ServletException { String authHeader = request.getHeader(GlobalUtil.AUTHORIZATION); String authToken = authHeader.substring(GlobalUtil.GRANT_TYPE.length()); String userJson = (String) LocalCache.getInstance().getValue(authToken); if (ObjectUtils.isEmpty(userJson)) { CommonResult commonResult = new CommonResult(); commonResult.setCode(YIXGResultEnum.OPERATE_FAILURE.getCode()) .setMessage(YIXGResultEnum.OPERATE_FAILURE.getMessage()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(commonResult)); response.getWriter().flush(); response.getWriter().close(); return; } MyUserDetail user = objectMapper.readValue(userJson, MyUserDetail.class); LocalCache.getInstance().putValue(authToken, "", 1); LogVO logVO = new LogVO(); logVO.setLogOperateUser(user.getUserName()) .setLogContent("登出成功") .setLogType("登入日誌"); logService.addLog(logVO); CommonResult commonResult = new CommonResult(); commonResult.setCode(YIXGResultEnum.OPERATE_SUCCESS.getCode()) .setMessage(YIXGResultEnum.OPERATE_SUCCESS.getMessage()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(commonResult)); response.getWriter().flush(); response.getWriter().close(); } }
Security核心配置:
@Configuration @EnableWebSecurity public class SecurityConfig { private final MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; private final MyAuthenticationFailureHandler myAuthenticationFailureHandler; private final MyLogoutSuccessHandler myLogoutSuccessHandler; private final UserDetailsService userDetailsService; public SecurityConfig( MyAuthenticationSuccessHandler myAuthenticationSuccessHandler, MyAuthenticationFailureHandler myAuthenticationFailureHandler, MyLogoutSuccessHandler myLogoutSuccessHandler, UserDetailsService userDetailsService ) { this.myAuthenticationSuccessHandler = myAuthenticationSuccessHandler; this.myAuthenticationFailureHandler = myAuthenticationFailureHandler; this.myLogoutSuccessHandler = myLogoutSuccessHandler; this.userDetailsService = userDetailsService; } @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration authenticationConfiguration ) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new MyPasswordEncoder(); } @Bean public MyAuthenticationTokenFilter myAuthenticationTokenFilter() { return new MyAuthenticationTokenFilter(); } @Bean SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity // 禁用basic明文驗證 .httpBasic(Customizer.withDefaults()) // 基於 token ,不需要 csrf .csrf(AbstractHttpConfigurer::disable) // 禁用預設登入頁 .formLogin(fl -> fl.loginProcessingUrl("/login") .usernameParameter("username") .passwordParameter("password") .successHandler(myAuthenticationSuccessHandler) .failureHandler(myAuthenticationFailureHandler) .permitAll()) // 禁用預設登出頁 .logout(lt -> lt.logoutSuccessHandler(myLogoutSuccessHandler)) // 基於 token , 不需要 session .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 設定 處理鑑權失敗、認證失敗 .exceptionHandling( exceptions -> exceptions.authenticationEntryPoint(new MyAuthenticationEntryPoint()) .accessDeniedHandler(new MyAccessDeniedHandler()) ) // 下面開始設定許可權 .authorizeHttpRequests(authorizeHttpRequest -> authorizeHttpRequest // 允許所有 OPTIONS 請求 .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 允許直接訪問 授權登入介面 .requestMatchers(HttpMethod.POST, "/web/authenticate").permitAll() // 允許 SpringMVC 的預設錯誤地址匿名訪問 .requestMatchers("/error").permitAll() // 其他所有介面必須有Authority資訊,Authority在登入成功後的UserDetailImpl物件中預設設定“ROLE_USER” //.requestMatchers("/**").hasAnyAuthority("ROLE_USER") .requestMatchers("/heartBeat/**", "/main/**").permitAll() // 允許任意請求被已登入使用者訪問,不檢查Authority .anyRequest().authenticated() ) // 新增過濾器 .addFilterBefore(myAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class) .build(); } @Bean public UserDetailsService userDetailsService() { return userDetailsService::loadUserByUsername; } /** * 呼叫loadUserByUserName獲取userDetail資訊,在AbstractUserDetailsAuthenticationProvider裡執行使用者狀態檢查 * * @return */ @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } // @Bean // public WebSecurityCustomizer webSecurityCustomizer() { // return (web) -> web.ignoring().requestMatchers(); // } /** * 配置跨源訪問(CORS) * * @return */ @Bean CorsConfigurationSource corsConfigurationSource() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); return source; } }
轉: https://blog.csdn.net/qq_40107343/article/details/136086463