在本例中,主要講解spring-boot與spring-security的集成,實現方式為:
- 將用戶、權限、資源(url)采用數據庫存儲
- 自定義過濾器,代替原有的 FilterSecurityInterceptor
- 自定義實現 UserDetailsService、AccessDecisionManager和InvocationSecurityMetadataSourceService,並在配置文件進行相應的配置
GitHub 地址:https://github.com/fp2952/spring-boot-security-demo
用戶角色表(基於RBAC權限控制)
- 用戶表(base_user)
code | type | length |
---|---|---|
ID | varchar | 32 |
USER_NAME | varchar | 50 |
USER_PASSWORD | varchar | 100 |
NIKE_NAME | varchar | 50 |
STATUS | int | 11 |
- 用戶角色表(base_user_role)
code | type | length |
---|---|---|
ID | varchar | 32 |
USER_ID | varchar | 32 |
ROLE_ID | varchar | 32 |
- 角色表(base_role)
code | type | length |
---|---|---|
ID | varchar | 32 |
ROLE_CODE | varchar | 32 |
ROLE_NAME | varchar | 64 |
- 角色菜單表(base_role_menu)
code | type | length |
---|---|---|
ID | varchar | 32 |
ROLE_ID | varchar | 32 |
MENU_ID | varchar | 32 |
- 菜單表(base_menu)
code | type | length |
---|---|---|
ID | varchar | 32 |
MENU_URL | varchar | 120 |
MENU_SEQ | varchar | 120 |
MENU_PARENT_ID | varchar | 32 |
MENU_NAME | varchar | 50 |
MENU_ICON | varchar | 20 |
MENU_ORDER | int | 11 |
IS_LEAF | varchar | 20 |
實現主要配置類
實現AbstractAuthenticationProcessingFilter
用於用戶表單驗證,內部調用了authenticationManager完成認證,根據認證結果執行successfulAuthentication或者unsuccessfulAuthentication,無論成功失敗,一般的實現都是轉發或者重定向等處理。
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
//獲取表單中的用戶名和密碼
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//組裝成username+password形式的token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//交給內部的AuthenticationManager去認證,並返回認證信息
return this.getAuthenticationManager().authenticate(authRequest);
}
AuthenticationManager
AuthenticationManager是一個用來處理認證(Authentication)請求的接口。在其中只定義了一個方法authenticate(),該方法只接收一個代表認證請求的Authentication對象作為參數,如果認證成功,則會返回一個封裝了當前用戶權限等信息的Authentication對象進行返回。
Authentication authenticate(Authentication authentication) throws AuthenticationException;
在Spring Security中,AuthenticationManager的默認實現是ProviderManager,而且它不直接自己處理認證請求,而是委托給其所配置的AuthenticationProvider列表,然后會依次使用每一個AuthenticationProvider進行認證,如果有一個AuthenticationProvider認證后的結果不為null,則表示該AuthenticationProvider已經認證成功,之后的AuthenticationProvider將不再繼續認證。然后直接以該AuthenticationProvider的認證結果作為ProviderManager的認證結果。如果所有的AuthenticationProvider的認證結果都為null,則表示認證失敗,將拋出一個ProviderNotFoundException。
校驗認證請求最常用的方法是根據請求的用戶名加載對應的UserDetails,然后比對UserDetails的密碼與認證請求的密碼是否一致,一致則表示認證通過。
Spring Security內部的DaoAuthenticationProvider就是使用的這種方式。其內部使用UserDetailsService來負責加載UserDetails。在認證成功以后會使用加載的UserDetails來封裝要返回的Authentication對象,加載的UserDetails對象是包含用戶權限等信息的。認證成功返回的Authentication對象將會保存在當前的SecurityContext中。
實現UserDetailsService
UserDetailsService只定義了一個方法 loadUserByUsername,根據用戶名可以查到用戶並返回的方法。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.debug("權限框架-加載用戶");
List<GrantedAuthority> auths = new ArrayList<>();
BaseUser baseUser = new BaseUser();
baseUser.setUserName(username);
baseUser = baseUserService.selectOne(baseUser);
if (baseUser == null) {
logger.debug("找不到該用戶 用戶名:{}", username);
throw new UsernameNotFoundException("找不到該用戶!");
}
if(baseUser.getStatus()==2)
{
logger.debug("用戶被禁用,無法登陸 用戶名:{}", username);
throw new UsernameNotFoundException("用戶被禁用!");
}
List<BaseRole> roles = baseRoleService.selectRolesByUserId(baseUser.getId());
if (roles != null) {
//設置角色名稱
for (BaseRole role : roles) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleCode());
auths.add(authority);
}
}
return new org.springframework.security.core.userdetails.User(baseUser.getUserName(), baseUser.getUserPassword(), true, true, true, true, auths);
}
實現AbstractSecurityInterceptor
訪問url時,會被AbstractSecurityInterceptor攔截器攔截,然后調用FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部權限,再調用授權管理器AccessDecisionManager鑒權。
public class CustomSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
public void invoke(FilterInvocation fi) throws IOException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} catch (ServletException e) {
super.afterInvocation(token, null);
}
}
public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
return securityMetadataSource;
}
public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource securityMetadataSource) {
this.securityMetadataSource = securityMetadataSource;
}
}
FilterInvocationSecurityMetadataSource 獲取所需權限
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//獲取當前訪問url
String url = ((FilterInvocation) object).getRequestUrl();
int firstQuestionMarkIndex = url.indexOf("?");
if (firstQuestionMarkIndex != -1) {
url = url.substring(0, firstQuestionMarkIndex);
}
List<ConfigAttribute> result = new ArrayList<>();
try {
//設置不攔截
if (propertySourceBean.getProperty("security.ignoring") != null) {
String[] paths = propertySourceBean.getProperty("security.ignoring").toString().split(",");
//判斷是否符合規則
for (String path: paths) {
String temp = StringUtil.clearSpace(path);
if (matcher.match(temp, url)) {
return SecurityConfig.createList("ROLE_ANONYMOUS");
}
}
}
//如果不是攔截列表里的, 默認需要ROLE_ANONYMOUS權限
if (!isIntercept(url)) {
return SecurityConfig.createList("ROLE_ANONYMOUS");
}
//查詢數據庫url匹配的菜單
List<BaseMenu> menuList = baseMenuService.selectMenusByUrl(url);
if (menuList != null && menuList.size() > 0) {
for (BaseMenu menu : menuList) {
//查詢擁有該菜單權限的角色列表
List<BaseRole> roles = baseRoleService.selectRolesByMenuId(menu.getId());
if (roles != null && roles.size() > 0) {
for (BaseRole role : roles) {
ConfigAttribute conf = new SecurityConfig(role.getRoleCode());
result.add(conf);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 判斷是否需要過濾
* @param url
* @return
*/
public boolean isIntercept(String url) {
String[] filterPaths = propertySourceBean.getProperty("security.intercept").toString().split(",");
for (String filter: filterPaths) {
if (matcher.match(StringUtil.clearSpace(filter), url) & !matcher.match(indexUrl, url)) {
return true;
}
}
return false;
}
AccessDecisionManager 鑒權
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
if (collection == null) {
return;
}
for (ConfigAttribute configAttribute : collection) {
String needRole = configAttribute.getAttribute();
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (needRole.trim().equals(ga.getAuthority().trim()) || needRole.trim().equals("ROLE_ANONYMOUS")) {
return;
}
}
}
throw new AccessDeniedException("無權限!");
}
配置 WebSecurityConfigurerAdapter
/**
* spring-security配置
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PropertySource propertySourceBean;
@Override
protected void configure(HttpSecurity http) throws Exception {
logger.debug("權限框架配置");
String[] paths = null;
//設置不攔截
if (propertySourceBean.getProperty("security.ignoring") != null) {
paths = propertySourceBean.getProperty("security.ignoring").toString().split(",");
paths = StringUtil.clearSpace(paths);
}
//設置過濾器
http // 根據配置文件放行無需驗證的url
.authorizeRequests().antMatchers(paths).permitAll()
.and()
.httpBasic()
// 配置驗證異常處理
.authenticationEntryPoint(getCustomLoginAuthEntryPoint())
// 配置登陸過濾器
.and().addFilterAt(getCustomLoginFilter(), UsernamePasswordAuthenticationFilter.class)
// 配置 AbstractSecurityInterceptor
.addFilterAt(getCustomSecurityInterceptor(), FilterSecurityInterceptor.class)
// 登出成功處理
.logout().logoutSuccessHandler(getCustomLogoutSuccessHandler())
// 關閉csrf
.and().csrf().disable()
// 其他所有請求都需要驗證
.authorizeRequests().anyRequest().authenticated()
// 配置登陸url, 登陸頁面並無需驗證
.and().formLogin().loginProcessingUrl("/login").loginPage("/login.ftl").permitAll()
// 登出
.and().logout().logoutUrl("/logout").permitAll();
logger.debug("配置忽略驗證url");
}
@Autowired
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(getDaoAuthenticationProvider());
}
/**
* spring security 配置
* @return
*/
@Bean
public CustomLoginAuthEntryPoint getCustomLoginAuthEntryPoint() {
return new CustomLoginAuthEntryPoint();
}
/**
* 用戶驗證
* @return
*/
@Bean
public DaoAuthenticationProvider getDaoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(new BCryptPasswordEncoder());
return provider;
}
/**
* 登陸
* @return
*/
@Bean
public CustomLoginFilter getCustomLoginFilter() {
CustomLoginFilter filter = new CustomLoginFilter();
try {
filter.setAuthenticationManager(this.authenticationManagerBean());
} catch (Exception e) {
e.printStackTrace();
}
filter.setAuthenticationSuccessHandler(getCustomLoginAuthSuccessHandler());
filter.setAuthenticationFailureHandler(new CustomLoginAuthFailureHandler());
return filter;
}
@Bean
public CustomLoginAuthSuccessHandler getCustomLoginAuthSuccessHandler() {
CustomLoginAuthSuccessHandler handler = new CustomLoginAuthSuccessHandler();
if (propertySourceBean.getProperty("security.successUrl")!=null){
handler.setAuthSuccessUrl(propertySourceBean.getProperty("security.successUrl").toString());
}
return handler;
}
/**
* 登出
* @return
*/
@Bean
public CustomLogoutSuccessHandler getCustomLogoutSuccessHandler() {
CustomLogoutSuccessHandler handler = new CustomLogoutSuccessHandler();
if (propertySourceBean.getProperty("security.logoutSuccessUrl")!=null){
handler.setLoginUrl(propertySourceBean.getProperty("security.logoutSuccessUrl").toString());
}
return handler;
}
/**
* 過濾器
* @return
*/
@Bean
public CustomSecurityInterceptor getCustomSecurityInterceptor() {
CustomSecurityInterceptor interceptor = new CustomSecurityInterceptor();
interceptor.setAccessDecisionManager(new CustomAccessDecisionManager());
interceptor.setSecurityMetadataSource(getCustomMetadataSourceService());
try {
interceptor.setAuthenticationManager(this.authenticationManagerBean());
} catch (Exception e) {
e.printStackTrace();
}
return interceptor;
}
@Bean
public CustomMetadataSourceService getCustomMetadataSourceService() {
CustomMetadataSourceService sourceService = new CustomMetadataSourceService();
if (propertySourceBean.getProperty("security.successUrl")!=null){
sourceService.setIndexUrl(propertySourceBean.getProperty("security.successUrl").toString());
}
return sourceService;
}
}