摘自:
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();
}
}

