先说说为什么写这个博客吧。
因为平时在开发过程中,只注意功能的开发和实现,对于踩得坑和技术点都没有时间进行整理,而且写一遍博客可以加深自己对自己写的功能的巩固和了解。我是自学的Spring boot(目前已经在慢慢应用到实际开发中),我原先的技术栈是SSM + shiro(shiro非本人集成),但是发现大部分公司已经转Spring boot里,我了解了一下 觉得Spring Boot这个框架还是挺好用的,毕竟有Spring全家桶,开发起来还是很顺手的。至于为什么不用Shiro,我想大概是我想多了解写技术(虽然shiro功能也不是我集成的,手动滑稽)。
接下来进入正题:
1. 首先引入Spring security的依赖 (我添加了好多依赖,暂时没时间整理)
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-openid</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-remoting</artifactId>
</dependency>
2. config大法 (不管是集成什么 boot都是添加对应的config文件)
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import com.zkh360.service.message.util.MD5Util;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
// 注入数据源
@Autowired
private DataSource dataSource;
@Bean
UserDetailsService customUserService() { // 注册UserDetailsService 的bean
return new MyUserDetailsService();
}
@Bean(name = "authenticationManager")
@Override
public AuthenticationManager authenticationManagerBean() {
AuthenticationManager authenticationManager = null;
try {
authenticationManager = super.authenticationManagerBean();
} catch (Exception e) {
e.printStackTrace();
}
return authenticationManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService()).passwordEncoder(new PasswordEncoder() {
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5Util.encode((String) rawPassword));
}
@Override
public String encode(CharSequence rawPassword) {
return MD5Util.encode((String) rawPassword);
}
}); // user Details Service验证;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭csrf保护功能(跨域访问)
.csrf().disable().authorizeRequests().anyRequest().authenticated() // 任何请求,登录后可以访问
.and().formLogin().loginPage("/login").failureUrl("/login?error").permitAll() // 登录页面用户任意访问
.and().logout().permitAll().invalidateHttpSession(true).and().rememberMe().tokenValiditySeconds(300)
// 指定记住登录信息所使用的数据源
.tokenRepository(tokenRepository());
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**");
web.ignoring().antMatchers("/js/**");
web.ignoring().antMatchers("/img/**");
web.ignoring().antMatchers("/webjars/**");
web.ignoring().antMatchers("**/favicon.ico");
web.ignoring().antMatchers("/ajax/**");
web.ignoring().antMatchers("/fonts/**");
web.ignoring().antMatchers("/less/**");
web.ignoring().antMatchers("/pages/**");
web.ignoring().antMatchers("/empty");
web.ignoring().antMatchers("/index");
web.ignoring().antMatchers("/error");
web.ignoring().antMatchers("/gallery/**");
web.ignoring().antMatchers("/assets/**");
web.ignoring().antMatchers("/resume");
}
// spring security 内部都写死了,这里要把 这个DAO 注入
@Bean
public JdbcTokenRepositoryImpl tokenRepository() {
JdbcTokenRepositoryImpl j = new JdbcTokenRepositoryImpl();
j.setDataSource(dataSource);
return j;
}
}
上述代码中,MyUserDetailsService可以通过@Autowired的方式注入进来,也可以写我的方式,MyUserDetailsService中是自定义的客户登录时的权限设置。configure(AuthenticationManagerBuilder auth)是进行秘密加密设计(如果不满足系统的加密方式可以自己设置加密规则),configure(HttpSecurity http)是配置相应的url过滤机制和验证机制,configure(WebSecurity web)是设置忽略的url(也可以放在前面设置),tokenRepository()是设置记住我功能时需要添加的。从这儿可以看出还缺少必应的权限校验部分(上面说的是登录部分)。
对了,以下是MyUserDetailsService,登录验证。
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
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.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.zkh360.service.message.domain.SysRoles;
import com.zkh360.service.message.mapper.RoleMapper;
import com.zkh360.service.message.mapper.UsersMapper;
/**
* 该类的主要作用是为Spring Security提供一个经过用户认证后的UserDetails。
* 该UserDetails包括用户名、密码、是否可用、是否过期等信息。
*/
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Autowired
private RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
//得到用户的权限
List<SysRoles> roleList = roleMapper.getRolesByUser(null, username, 0, 10000);
List<GrantedAuthority> authsList = new ArrayList<>();
for (SysRoles role : roleList) {
authsList.add(new SimpleGrantedAuthority(role.getRole_name()));
}
//取得用户的密码
String password = usersMapper.getPasswordByUsername(username);
return new org.springframework.security.core.userdetails.User(username, password, true, true, true, true, authsList);
}
}
3. 必要的filter(权限校验部分)
import java.io.IOException;
import javax.annotation.PostConstruct;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Service;
import com.zkh360.service.message.auth.manager.MyAccessDecisionManager;
@Service
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
private MyAccessDecisionManager myAccessDecisionManager;
@Autowired
private AuthenticationManager authenticationManager;
@PostConstruct
public void init(){
super.setAuthenticationManager(authenticationManager);
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
private void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
import java.util.Collection;
import java.util.Iterator;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
/**
* AccessdecisionManager在Spring security中是很重要的。
* <p>
* 在验证部分简略提过了,所有的Authentication实现需要保存在一个GrantedAuthority对象数组中。
* 这就是赋予给主体的权限。 GrantedAuthority对象通过AuthenticationManager
* 保存到 Authentication对象里,然后从AccessDecisionManager读出来,进行授权判断。
* <p>
* Spring Security提供了一些拦截器,来控制对安全对象的访问权限,例如方法调用或web请求。
* 一个是否允许执行调用的预调用决定,是由AccessDecisionManager实现的。
* 这个 AccessDecisionManager 被AbstractSecurityInterceptor调用,
* 它用来作最终访问控制的决定。 这个AccessDecisionManager接口包含三个方法:
* <p>
* void decide(Authentication authentication, Object secureObject,
* List<ConfigAttributeDefinition> config) throws AccessDeniedException;
* boolean supports(ConfigAttribute attribute);
* boolean supports(Class clazz);
* <p>
* 从第一个方法可以看出来,AccessDecisionManager使用方法参数传递所有信息,这好像在认证评估时进行决定。
* 特别是,在真实的安全方法期望调用的时候,传递安全Object启用那些参数。
* 比如,让我们假设安全对象是一个MethodInvocation。
* 很容易为任何Customer参数查询MethodInvocation,
* 然后在AccessDecisionManager里实现一些有序的安全逻辑,来确认主体是否允许在那个客户上操作。
* 如果访问被拒绝,实现将抛出一个AccessDeniedException异常。
* <p>
* 这个 supports(ConfigAttribute) 方法在启动的时候被
* AbstractSecurityInterceptor调用,来决定AccessDecisionManager
* 是否可以执行传递ConfigAttribute。
* supports(Class)方法被安全拦截器实现调用,
* 包含安全拦截器将显示的AccessDecisionManager支持安全对象的类型。
*/
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if (configAttributes == null) {
return;
}
Iterator<ConfigAttribute> ite = configAttributes.iterator();
while (ite.hasNext()) {
ConfigAttribute ca = ite.next();
String needRole = ((SecurityConfig) ca).getAttribute();
//ga 为用户所被赋予的权限。 needRole 为访问相应的资源应该具有的权限。
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (needRole.trim().equals(ga.getAuthority().trim())) {
return;
}
}
}
throw new AccessDeniedException("");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
不解释了 直接拿去用,下面是url和权限直接的校验
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
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.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Service;
import com.zkh360.service.message.domain.SysAuthorities;
import com.zkh360.service.message.mapper.AuthoritiesMapper;
import com.zkh360.service.message.mapper.ResourcesMapper;
import com.zkh360.service.message.model.SysResourcesVo;
@Service
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
@Autowired
private AuthoritiesMapper authoritiesMapper;
@Autowired
private ResourcesMapper resourcesMapper;
private void loadResourceDefine() {
// 在Web服务器启动时,提取系统中的所有权限。
List<SysAuthorities> authoritiesList = authoritiesMapper.selectAll();
/**//*
* 应当是资源为key, 权限为value。 资源通常为url, 权限就是那些以ROLE_为前缀的角色。 一个资源可以由多个权限来访问。
* sparta
*/
resourceMap = new HashMap<>();
for (SysAuthorities authority : authoritiesList) {
ConfigAttribute ca = new SecurityConfig(authority.getAuthority_name());
List<SysResourcesVo> resourcesList = resourcesMapper.loadResourcesByAuthority(authority.getAuthority_name());
for (SysResourcesVo res : resourcesList) {
/**//*
* 判断资源文件和权限的对应关系,如果已经存在相关的资源url,则要通过该url为key提取出权限集合,将权限增加到权限集合中。
* sparta
*/
if (resourceMap.containsKey(res.getResource_name())) {
Collection<ConfigAttribute> value = resourceMap.get(res.getResource_name());
value.add(ca);
resourceMap.put(res.getResource_name(), value);
} else {
Collection<ConfigAttribute> atts = new ArrayList<>();
atts.add(ca);
resourceMap.put(res.getResource_name(), atts);
}
}
}
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
// 根据URL,找到相关的权限配置。
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if(resourceMap == null) loadResourceDefine();
FilterInvocation filterInvocation = (FilterInvocation) object;
Iterator<String> ite = resourceMap.keySet().iterator();
while (ite.hasNext()) {
String requestURL = ite.next();
RequestMatcher requestMatcher = new AntPathRequestMatcher(requestURL);
if (requestMatcher.matches(filterInvocation.getHttpRequest())) {
return resourceMap.get(requestURL);
}
}
return null;
}
@Override
public boolean supports(Class<?> arg0) {
return true;
}
}