最近在项目当中使用spring security做权限管理,下面简单记录一下如何配置和使用.
一、权限相关表
1、user:用户表
2、groups:用户组表
3、role:角色表
4、resource:资源表
5、user_group:用户、组关系表
6、group_role:用户组、角色关系表
7、role_resource:角色、资源关系表
一个用户可以在多个组中,一个组可以包括多个角色、一个角色可以包括多个资源。
这里的资源就是权限管理的目标,包括页面上的元素、后台方法等等。
二、pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
spring-boot-starter-security是spring security核心包
thymeleaf-extras-springsecurity4是在thymeleaf中使用一些security标签的依赖包
三、在启动类中
因为springboot自动配置的原因,当加上Pom中的依赖后,系统自动就会加上一个默认的权限校验,给了一个随机的用户和密码,并且提供了一个登陆页面。在正常开发中,我们当然不会这么做,因此我们要关闭掉security的自动配置,如下:
@EnableAutoConfiguration(exclude = {SecurityAutoConfiguration.class})
四、配置spring security
1、
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@ConditionalOnProperty(name = "security.enabled")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private FilterSecurityInterceptor filterSecurityInterceptor;
@Bean
UserDetailsService customUserService() {
//注册UserDetailsService 的bean
return new CustomUserService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//user Details Service验证,密码md5加密
auth.userDetailsService(customUserService()).passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence password) {
return MD5Util.encode((String) password);
}
@Override
public boolean matches(CharSequence password, String encodedPassword) {
return encodedPassword.equals(MD5Util.encode((String) password));
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() //关闭默认的跨域保护
.authorizeRequests()
//放开静态资源
.antMatchers("/frame/**", "/i18n/**", "/images/**", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated() //任何请求,登录后可以访问
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error")
.permitAll() //登录页面用户任意访问
.and()
.logout()
.logoutSuccessUrl("/login")
.permitAll(); //注销行为任意访问
http.exceptionHandling().accessDeniedHandler(new SswAccessDeniedHandler());
http.addFilterBefore(filterSecurityInterceptor, org.springframework.security.web.access.intercept.FilterSecurityInterceptor.class);
}
}
说明一下:
@EnableWebSecurity 开启spring security
@EnableGlobalMethodSecurity(prePostEnabled = true) 在方法中可以使用@PreAuthorize("hasAuthority('admin')")等注解来控制某个方法的全年
FilterSecurityInterceptor自定义的拦截器,继承了AbstractSecurityInterceptor,security的功能基本上就是靠这个拦截器完成,后面会描述
CustomUserService自定义类,继承UserDetailsService,该类主要用来在登陆时加载用户信息及其权限信息给spring security.
http.exceptionHandling().accessDeniedHandler(new SswAccessDeniedHandler());自定义了当没有权限访问时的处理策略。
其他的在注释中都有描述,就不细说了
2、
@Component("customUserService")
@ConditionalOnProperty(name = "security.enabled")
public class CustomUserService implements UserDetailsService {
@Resource
private UserDao userDao;
@Resource
private ResourceDao resourceDao;
@Override
public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {
User user = userDao.findByLoginName(loginName);
if (user == null) {
throw new UsernameNotFoundException("用户: " + loginName + " 不存在!");
}
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
List<com.aas.ssw.business.example.entity.Resource> resourceList = resourceDao.findByUserId(user.getId());
if(resourceList == null || resourceList.size() == 0){
return new org.springframework.security.core.userdetails.User(user.getLoginName(), user.getPassword(), grantedAuthorities);
}
for (com.aas.ssw.business.example.entity.Resource resource : resourceList) {
if (resource != null && resource.getName() != null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(resource.getName());
//1:此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
grantedAuthorities.add(grantedAuthority);
}
}
return new org.springframework.security.core.userdetails.User(user.getLoginName(), user.getPassword(), grantedAuthorities);
}
}
当有用户登陆时,spring security会调用loadUserByUsername方法,并把用户输入的账号传进来,但是并不传密码,因为这个方法不会做用户名和密码的校验,该方法只是根据用户名从数据库中查出来用户的信息,然后将其交给spring security来根据这个信息和用户输入的账号密码来校验登陆是否成功。如果登陆成功,那么spring security会将用户信息、权限等保存在内存中,以便后边使用。也就是说,登陆校验是由spring security来做的,不需要我们显式的处理。
3、
@Component("filterInvocationSecurityMetadataSource")
@ConditionalOnProperty(name = "security.enabled")
public class InvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
@Resource
private ResourceDao resourceDao;
private ConcurrentHashMap<String, List<ConfigAttribute>> map = new ConcurrentHashMap<>();
/**
* 加载资源表中所有资源
*/
@PostConstruct
private void loadAllResource() {
List<com.aas.ssw.business.example.entity.Resource> resourceList = resourceDao.findAll();
for (com.aas.ssw.business.example.entity.Resource resource : resourceList) {
ConfigAttribute securityConfig = new SecurityConfig(resource.getName());
List<ConfigAttribute> configAttributeList = map.get(resource.getUrl());
if(configAttributeList == null || configAttributeList.size() == 0){
configAttributeList = new ArrayList<>();
configAttributeList.add(securityConfig);
map.put(resource.getUrl(), configAttributeList);
}else {
configAttributeList.add(securityConfig);
}
}
}
/**
* 此方法是为了判定用户请求的url是否在资源表中,
* 如果在资源表中,则返回给 decide 方法,用来判定用户是否有此权限。
* 如果不在权限表中则放行。
* @param o
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//object 中包含用户请求的request 信息
HttpServletRequest request = ((FilterInvocation) o).getHttpRequest();
for (String resourceUrl : map.keySet()) {
AntPathRequestMatcher matcher = new AntPathRequestMatcher(resourceUrl);
if(matcher.matches(request)) {
return map.get(resourceUrl);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
该类有两个作用,第一个是在系统初始化的时候加载所有需要校验的资源。也就是说在这里被加载的资源在访问时会被spring security校验。第二个时每当有访问请求时,会判断请求的资源是不是需要校验,如果时则交给AccessDecisionManager来做判断,如果不是则直接放行。
4、
@Component("accessDecisionManager")
@ConditionalOnProperty(name = "security.enabled")
public class SswAccessDecisionManager implements AccessDecisionManager {
/**
* decide 方法是判定是否拥有权限的决策方法,
* authentication 是用户拥有的所有资源.
* object 包含客户端发起的请求的requset信息;
* configAttributes 为InvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果 即需要检验的资源
*
* @param authentication
* @param object
* @param configAttributes
* @throws AccessDeniedException
* @throws InsufficientAuthenticationException
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if (null == configAttributes || configAttributes.size() <= 0) {
return;
}
for (ConfigAttribute configAttribute : configAttributes) {
String needRole = configAttribute.getAttribute();
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
//authentication 为在注释1 中循环添加到 GrantedAuthority 对象中的权限信息集合
if (needRole.trim().equals(grantedAuthority.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("没有权限");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
该类的主要作用就是校验用户是否拥有要访问的资源的权限,如果有则可以访问,否则则抛出异常,该异常会被AccessDeniedHandler捕获处理。
5、
/**
* 自定义security没有权限时的处理策略
*/
public class SswAccessDeniedHandler implements AccessDeniedHandler {
private final String errorPage = "/accessDeny";
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
boolean isAjax = RequestUtil.isAjaxRequest(request);
//ajax请求返回json数据,非ajax请求跳转至自定义页面
if (isAjax) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = null;
try {
out = response.getWriter();
out.append(JSONObject.toJSONString(Result.getResult(Constant.FAIL,"没有权限",null,null)));
out.flush();
} finally {
if(out != null){
out.close();
}
}
} else if (!response.isCommitted()) {
if (errorPage != null) {
// Put exception into request scope (perhaps of use to a view)
request.setAttribute(WebAttributes.ACCESS_DENIED_403, e);
// Set the 403 status code.
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// forward to error page.
RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
dispatcher.forward(request, response);
} else {
response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
}
}
}
}
该类用来处理没有权限时的应该怎么做,这里主要时区分了一下是否是ajax请求,如果时则返回一个json,不是则跳转到自定义的页面中。
6、
@Component("filterSecurityInterceptor")
@ConditionalOnProperty(name = "security.enabled")
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Resource
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Resource
public void setMyAccessDecisionManager(SswAccessDecisionManager accessDecisionManager) {
super.setAccessDecisionManager(accessDecisionManager);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation filterInvocation = new FilterInvocation(request, response, filterChain);
invoke(filterInvocation);
}
private void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
//filterInvocation里面有一个被拦截的url
//里面调用InvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取filterInvocation对应的所有权限
//再调用SswAccessDecisionManager的decide方法来校验用户的权限是否足够
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
//执行下一个拦截器
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
主要靠该拦截器完成权限校验,具体来说,该拦截器使用前边定义的AccessDecisionManager,InvocationSecurityMetadataSourceService完成校验工作。
五、使用
主要有两种使用场景
1、页面当中的元素:
<div sec:authorize="hasAuthority('admin')">管理员才能看见</div>
<div sec:authorize="hasAuthority('user')">管理员和普通用户都能看见</div>
2、某个具体的方法:
@GetMapping("/test3")
@ResponseBody
@PreAuthorize("hasAuthority('admin')")
public String test3(){
return "hehe";
}
都很简单,就不解释了。