摘自:
https://www.cnblogs.com/hhhshct/p/9726378.html
https://blog.csdn.net/weixin_42849689/article/details/89957823
https://blog.csdn.net/zhaoxichen_10/article/details/88713799
http://www.imooc.com/article/287214
一、Spring Security簡介
Spring Security是一個能夠為基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能,為應用系統提供聲明式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重復代碼的工作。它是一個輕量級的安全框架,它確保基於Spring的應用程序提供身份驗證和授權支持。它與Spring MVC有很好地集成,並配備了流行的安全算法實現捆綁在一起。安全主要包括兩個操作“認證”與“驗證”(有時候也會叫做權限控制)。“認證”是為用戶建立一個其聲明的角色的過程,這個角色可以一個用戶、一個設備或者一個系統。“驗證”指的是一個用戶在你的應用中能夠執行某個操作。在到達授權判斷之前,角色已經在身份認證過程中建立了。
用戶登陸,會被AuthenticationProcessingFilter攔截,調用AuthenticationManager的實現,而且AuthenticationManager會調用ProviderManager來獲取用戶驗證信息(不同的Provider調用的服務不同,因為這些信息可以是在數據庫上,可以是在LDAP服務器上,可以是xml配置文件上等),如果驗證通過后會將用戶的權限信息封裝一個User放到spring的全局緩存SecurityContextHolder中,以備后面訪問資源時使用。
訪問資源(即授權管理),訪問url時,會通過AbstractSecurityInterceptor攔截器攔截,其中會調用FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部權限,在調用授權管理器AccessDecisionManager,這個授權管理器會通過spring的全局緩存SecurityContextHolder獲取用戶的權限信息,還會獲取被攔截的url和被攔截url所需的全部權限,然后根據所配的策略(有:一票決定,一票否定,少數服從多數等),如果權限足夠,則返回,權限不夠則報錯並調用權限不足頁面。
二、Spring Security的執行過程
三、Spring Security代碼實現
Spring Security的核心配置類是 WebSecurityConfig,抽象類
這是權限管理啟動的入口,這里我們自定義一個實現類去它。然后編寫我們需要處理的控制邏輯。
下面是代碼,里面寫的注釋也比較詳細。在里面還依賴了幾個自定義的類,都是必須配置的。分別是
userService,
myFilterInvocationSecurityMetadataSource,
myAccessDecisionManager,
authenticationAccessDeniedHandler
3.1 WebSecurityConfig
package com.example.demo.config; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import com.example.demo.service.UserService; /** * spring-security權限管理的核心配置 * @author wjqhuaxia * */ @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) //全局 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService;//實現了UserDetailsService接口 @Autowired private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;//權限過濾器(當前url所需要的訪問權限) @Autowired private MyAccessDecisionManager myAccessDecisionManager;//權限決策器 @Autowired private AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;//自定義錯誤(403)返回數據 /** * 自定義的加密算法 * @return */ @Bean public PasswordEncoder myPasswordEncoder() { return new MyPasswordEncoder(); } /** * 配置userDetails的數據源,密碼加密格式 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(myPasswordEncoder()); } /** * 配置放行的資源 */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring() .antMatchers("/index.html", "/static/**","/loginPage","/register") // 給 swagger 放行;不需要權限能訪問的資源 .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/images/**", "/webjars/**", "/v2/api-docs", "/configuration/ui", "/configuration/security"); } /** * 這段配置,我認為就是配置Security的認證策略, 每個模塊配置使用and結尾。 authorizeRequests()配置路徑攔截,表明路徑訪問所對應的權限,角色,認證信息。 formLogin()對應表單認證相關的配置 logout()對應了注銷相關的配置 httpBasic()可以配置basic登錄 */ /** * HttpSecurity包含了原數據(主要是url) * 1.通過withObjectPostProcessor將MyFilterInvocationSecurityMetadataSource和MyAccessDecisionManager注入進來 * 2.此url先被MyFilterInvocationSecurityMetadataSource處理,然后 丟給 MyAccessDecisionManager處理 * 3.如果不匹配,返回 MyAccessDeniedHandler */ @Override protected void configure(HttpSecurity http) throws Exception { // authorizeRequests()配置路徑攔截,表明路徑訪問所對應的權限,角色,認證信息 http.authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource); o.setAccessDecisionManager(myAccessDecisionManager); return o; } }) .and() // formLogin()對應表單認證相關的配置 .formLogin() .loginPage("/loginPage") .loginProcessingUrl("/login") .usernameParameter("username") .passwordParameter("password") .permitAll() .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); StringBuffer sb = new StringBuffer(); sb.append("{\"status\":\"error\",\"msg\":\""); if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) { sb.append("用戶名或密碼輸入錯誤,登錄失敗!"); } else { sb.append("登錄失敗!"); } sb.append("\"}"); out.write(sb.toString()); out.flush(); out.close(); } }).successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); String s = "{\"status\":\"success\",\"msg\":\"登陸成功\"}"; out.write(s); out.flush(); out.close(); } }).and() // logout()對應了注銷相關的配置 .logout() .permitAll() .and() .csrf() .disable() .exceptionHandling() .accessDeniedHandler(authenticationAccessDeniedHandler); } }
3.2 UserService
UserServiceImpl實現了UserDetailsService接口中的loadUserByUsername方法,方法執行成功后返回UserDetails對象,為構建Authentication對象提供必須的信息。UserDetails中包含了用戶名,密碼,角色等信息。
package com.example.demo.service.impl; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.demo.dao.PermissionMapper; import com.example.demo.dao.RoleMapper; import com.example.demo.dao.UserMapper; import com.example.demo.model.Permission; import com.example.demo.model.User; import com.example.demo.service.UserService; /** * 實現了UserDetailsService接口中的loadUserByUsername方法 * 執行登錄,構建Authentication對象必須的信息, * 如果用戶不存在,則拋出UsernameNotFoundException異常 * @author wjqhuaxia * */ @Service public class UserServiceImpl implements UserService { @Autowired private PermissionMapper permissionMapper; @Autowired private RoleMapper roleMapper; @Autowired private UserMapper userMapper; @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.selectByUsername(username); if (user != null) { List<Permission> permissions = permissionMapper.findByUserId(user.getId()); List<GrantedAuthority> grantedAuthorities = new ArrayList <>(); for (Permission permission : permissions) { if (permission != null && permission.getPermissionname()!=null) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getPermissionname()); grantedAuthorities.add(grantedAuthority); } } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities); } else { throw new UsernameNotFoundException("username: " + username + " do not exist!"); } } @Transactional @Override public void userRegister(String username, String password) { User user = new User(); user.setUsername(passwordEncoder.encode(username)); user.setPassword(password); userMapper.insert(user); User rtnUser =userMapper.selectByUsername(username); //注冊成功默認給用戶的角色是user roleMapper.insertUserRole(rtnUser.getId(), 2); } }
3.3 MyFilterInvocationSecurityMetadataSource
自定義權限過濾器,繼承了 SecurityMetadataSource(權限資源接口),過濾所有請求,核查這個請求需要的訪問權限;主要實現Collection<ConfigAttribute> getAttributes(Object o)方法,此方法中可編寫用戶邏輯,根據用戶預先設定的用戶權限列表,返回訪問此url需要的權限列表。
package com.example.demo.config; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import com.example.demo.dao.PermissionMapper; import com.example.demo.model.Permission; /** * 自定義權限過濾器 * FilterInvocationSecurityMetadataSource(權限資源過濾器接口)繼承了 SecurityMetadataSource(權限資源接口) * Spring Security是通過SecurityMetadataSource來加載訪問時所需要的具體權限;Metadata是元數據的意思。 * 自定義權限資源過濾器,實現動態的權限驗證 * 它的主要責任就是當訪問一個url時,返回這個url所需要的訪問權限 * @author wjqhuaxia * */ @Service public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private static final Logger log = LoggerFactory.getLogger(MyFilterInvocationSecurityMetadataSource.class); @Autowired private PermissionMapper permissionMapper; private HashMap<String, Collection<ConfigAttribute>> map = null; /** * 加載權限表中所有權限 */ public void loadResourceDefine() { map = new HashMap<String, Collection<ConfigAttribute>>(); List<Permission> permissions = permissionMapper.findAll(); for (Permission permission : permissions) { if(StringUtils.isEmpty(permission.getPermissionname())){ continue; } if(StringUtils.isEmpty(permission.getUrl())){ continue; } ConfigAttribute cfg = new SecurityConfig(permission.getPermissionname()); List<ConfigAttribute> list = new ArrayList<>(); list.add(cfg); // TODO:如果一個url對應多個權限,這里有問題 map.put(permission.getUrl(), list); } } /** * 此方法是為了判定用戶請求的url 是否在權限表中,如果在權限表中,則返回給 decide 方法, 用來判定用戶 * 是否有此權限。如果不在權限表中則放行。 */ @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { if (map == null) { loadResourceDefine(); } // object 中包含用戶請求的request的信息 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest(); for (Entry<String, Collection<ConfigAttribute>> entry : map.entrySet()) { String url = entry.getKey(); if (new AntPathRequestMatcher(url).matches(request)) { return map.get(url); } } /** * @Author: Galen * @Description: 如果本方法返回null的話,意味着當前這個請求不需要任何角色就能訪問 * 此處做邏輯控制,如果沒有匹配上的,返回一個默認具體權限,防止漏缺資源配置 **/ log.info("當前訪問路徑是{},這個url所需要的訪問權限是{}", request.getRequestURL(), "ROLE_LOGIN"); return SecurityConfig.createList("ROLE_LOGIN"); } /** * 此處方法如果做了實現,返回了定義的權限資源列表, * Spring Security會在啟動時校驗每個ConfigAttribute是否配置正確, * 如果不需要校驗,這里實現方法,方法體直接返回null即可 */ @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } /** * 方法返回類對象是否支持校驗, * web項目一般使用FilterInvocation來判斷,或者直接返回true */ @Override public boolean supports(Class<?> clazz) { return true; } }
3.4 AuthenticationAccessDeniedHandler
自定義權限決策管理器,需要實現AccessDecisionManager 的 void decide(Authentication auth, Object object, Collection<ConfigAttribute> cas) 方法,在上面的過濾器中,我們已經得到了訪問此url需要的權限;那么,decide方法,先查詢此用戶當前擁有的權限,然后與上面過濾器核查出來的權限列表作對比,以此判斷此用戶是否具有這個訪問權限,決定去留!所以顧名思義為權限決策器。
package com.example.demo.config; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * 拒簽(403響應)處理器 * Denied是拒簽的意思 * @author wjqhuaxia * */ @Component public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); resp.setContentType("application/json;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.write("{\"status\":\"error\",\"msg\":\"權限不足,請聯系管理員!\"}"); out.flush(); out.close(); } }