package com.study.config;
import org.springframework.security.web.context.*;
public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
@Override
protected Class>[] getRootConfigClasses() {
return new Class[] { WebSecurityConfig.class };
}
}
继承该类相当于在web.xml中:
springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy
重写getRootConfigClasses()相当于把下一步Spring Security的配置加载到程序中。
package com.study.config;
import javax.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.study.security.MyFilterSecurityInterceptor;
@Configuration
@EnableWebSecurity //启用web安全功能
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// @Resource
// private DataSource dataSource;
//配置自定义的用户服务(即选择用户信息的查询方式是从用户库或数据库,怎么查询)
@Resource(name="myUserDetailService")
private UserDetailsService myUserDetailService;
//第3点:自定义用户验证
@Resource
private AuthenticationProvider myAuthenticationProvider;
//自定义认证成功处理器
@Resource
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
//第4点:自定义拦截器(自定义权限验证,而不是拦截规则)
@Resource
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
/**
* 密码加密
*/
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 自定义认证过程
*/
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
/* 不重写的话默认为
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER"); //默认准备了一个用户
*/
//方式一:内存用户存储
/*
auth.inMemoryAuthentication()
.withUser("user1").password("123456").roles("USER").and()
.withUser("admin").password("admin").roles("USER","ADMIN");
*/
//
//方式二:数据库表认证
/*
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select username,password,enable from t_user where username=?")
.authoritiesByUsernameQuery("select username,rolename from t_role where username=?");
*/
//方式三:自定义数据库
auth.userDetailsService(myUserDetailService)
.passwordEncoder(new Md5PasswordEncoder());
//加后MyAuthenticationProvider里比对密码时能把表单的明文和数据库的密文对应
//自定义用户验证
auth.authenticationProvider(myAuthenticationProvider);
}
// 暴露默认的authenticationManager
// @Override
// public AuthenticationManager authenticationManagerBean() throws Exception {
// return super.authenticationManagerBean();
// }
/**
* 配置Spring Security的Filter链
*/
@Override
public void configure(WebSecurity web)throws Exception {
// 设置不拦截规则
web.ignoring().antMatchers("/css/**","/js/**","/img/**","/font-awesome/**");
}
/**
* 配置如何通过拦截器保护请求:配置了自定义的过滤器、自定义登录登出页面
* 前端请求的时候先经过这里检测请求是否需要验证或权限,不需要的话直接把请求分发给controller去处理,需要就认证过了再分发
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(myFilterSecurityInterceptor,
FilterSecurityInterceptor.class)//在正确的位置添加我们自定义的过滤器
.authorizeRequests() //需要权限,权限符合与否由上面过滤器检查
.antMatchers("/admin/**").hasRole("ADMIN") //以admin开头的url需要admin权限
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().authenticated() //不与以上匹配的所有请求只需要登录验证
// .and().formLogin().and() //and相当于节点结束
// .httpBasic();
// 自定义登录页面:permitAll,允许all用户访问login页面
//未登录前访问受保护资源被转回login
.and()
/*直接/jsp/login的话会分配给controller去处理,则需要写controller映射到登录页;请求login.jsp报404
有后缀则直接请求资源,那么url=/login还是由controller处理;
那么最好是加后缀,才不用再走controller*/
.formLogin().loginPage("/jsp/login.jsp")
.failureUrl("/jsp/login.jsp?error=1")
//表单提交地址,4.x默认为/login;无需认证,但是请求方式必须post
.loginProcessingUrl("/spring_security_check")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(myAuthenticationSuccessHandler) //自定义认证成功处理器
//.failureHandler(myAuthenticationFailureHandler)
.permitAll()
//这里也是,要么配资源路径,要么配有controller处理的url
.defaultSuccessUrl("/index.do")
.and()
//配置session管理
.sessionManagement()
/*指定了maximumSessions需配合web.xml中HttpSessionEventPublisher
清理掉过期session*/
.maximumSessions(1)
.expiredUrl("/login.jsp?expired=1");
//如果(默认)开启了CSRF,退出则需要使用POST访问,可使用以下方式解决,但不推荐
http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
//登陆成功后跳转的地址,以及删除的cookie名称
.logoutSuccessUrl("/jsp/login.jsp?error=logout")
.invalidateHttpSession(true);
}
}
所以给请求设置权限,如果没有指定权限,则至少不能是匿名用户
大致相当于配置
//起别名,方便被下面自定义拦截器依赖
简单的自定义登录页面对比
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
相当于
自定义权限
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() //需要权限
.antMatchers("/resources/**", "/signup", "/about").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
//使用hasRole()可以不写ROLE_前缀,但实际上我们指定的权限还是ROLE_ADMIN
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
/*按照顺序匹配,下面是对的*/
//.antMatchers("/admin/**").hasRole("ADMIN")
//.antMatchers("/**").hasRole("USER")
/*下面是错的,因为满足第一个规则后将不会检查第二条,所以要从精确到广泛配置*/
//.antMatchers("/**").hasRole("USER")
//.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated() //需要验证
.and()
// ...
.formLogin();
}
自定义登出页面
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutUrl("/my/logout")
.logoutSuccessUrl("/my/index")
.logoutSuccessHandler(logoutSuccessHandler) //自定义登出成功后的操作,上一行失效
.invalidateHttpSession(true)
//添加一个自定义登出处理器,SecurityContextLogoutHandler默认是最后一个处理器
.addLogoutHandler(logoutHandler)
.deleteCookies(cookieNamesToClear)
.and()
...
}
一般前面两步都是直接用配置文件实现,然后注入自定义UserDetailServiceImpl,也就这个要用java写而已
如果是要用数据库的话那肯定是要的,首先是用户实体类UserDetails必须改为符合数据库的字段,默认的也就用户名、密码加权限列表了,其次获取用户信息UserDetialsService的方式也要改成从数据库获取.
首先是UserDetailsService,主要是根据用户名从数据库找出用户all信息,主要是用户名、密码和权限列表
package com.study.security;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Resource;
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.Component;
import com.study.model.Resources;
import com.study.model.User;
import com.study.service.ResourcesService;
import com.study.service.UserService;
@Component("myUserDetailService")
public class MyUserDetailServiceImpl implements UserDetailsService{
//这里可以来个日志
@Resource
private UserService userService;
@Resource
private ResourcesService resourcesService;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
//通过用户名查询用户信息
User user = userService.findUserByName(username);
//系统实体类User implement UsersService,系统service类userService
if(user ==null)
throw new UsernameNotFoundException(username+" not exist!");
//存放角色名的集合
Set authSet = new HashSet();
//系统实体类Resources,即各个url
Resources resources = new Resources();
resources.setUsername(username);
//找到特定用户名能访问的url集合
List list = resourcesService.loadMenu(resources);
for (Resources r : list) {
//根据资源找到角色名,添加到角色名集合
authSet.add(new SimpleGrantedAuthority("ROLE_" +r.getResKey()));
//resKey:资源名,拼成角色名
}
/*
username():登录用户名
password():从数据库查出来的密码
enable: 用户状态,true为有效,false无效
accoutNonExpired,credentialsNonExpired,accountNonLocked
roles: Collections集合,当前登录用户的角色列表
*/
return new org.springframework.security.core.userdetails.User(user.getUsername(),
user.getPassword(),
user.getEnable()==1?true:false,
true,
true,
true,
authSet); //返回的user不为空,则验证通过
}
}
系统实体类User实现UserDetails,主要是查询数据库所有权限,返回权限列表
public class User implements UserDetails {
private static final long serialVersionUID = 8026813053768023527L;
private String name;
private String password;
private Set roles; //可以不写
//其他属性,随便,自定义
/**
* 返回系统所有角色
*/
@Override
public Collection extends GrantedAuthority> getAuthorities() {
//根据自定义逻辑来返回用户权限,如果用户权限返回空或者和拦截路径对应权限不同,验证不通过
if(!roles.isEmpty()){
List list = new ArrayList();
GrantedAuthority au = new SimpleGrantedAuthority(role.getrName());
list.add(au);
return list;
}
return null;
}
/**
* 默认有一个方法返回当前用户的角色列表,后面介绍了它的重写;可选的
*/
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
/*
*帐号是否不过期,false则验证不通过
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/*
* 帐号是否不锁定,false则验证不通过
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/*
* 凭证是否不过期,false则验证不通过
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/*
* 该帐号是否启用,false则验证不通过
*/
@Override
public boolean isEnabled() {
return true;
}
}
User类代码实例:llcweb/domain/models/
数据库表介绍:五张表————用户表、角色表、资源表、用户角色表、角色资源表。给用户分配角色,给角色分配资源(即权限)。一个用户可以对应多个角色,一个角色可以对应多个资源。
我们这一节说的过滤器特指这个FilterSecurityInterceptor。基于数据库的权限验证必须要自定义过滤器,因为它主要就是做权限验证的,包括资源与权限对应关系的查询、当前用户是否持有请求资源所需权限的判断。
前面不是用java注册spring security的话就需要以下配置
开始自定义过滤器
package com.study.security;
import java.io.IOException;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
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.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements
Filter {
//所有资源与权限的关系(需要在外面自定义):获取请求资源的权限
@Autowired
private MySecurityMetadataSource securityMetadataSource;
//用户是否拥有所请求资源的权限(需要在外面自定义):判断是否拥有请求资源所需的权限
@Autowired
private MyAccessDecisionManager accessDecisionManager;
/* 默认装配够用了
@Resource(name="myAuthenticationManager")
private AuthenticationManager authenticationManager;
*/
@Resource
private AuthenticationConfiguration authenticationConfiguration;
// @Autowired
// public void setAuthenticationConfiguration(AuthenticationConfiguration authenticationConfiguration) {
// this.authenticationConfiguration = authenticationConfiguration;
// }
@PostConstruct
public void init() throws Exception{
super.setAccessDecisionManager(accessDecisionManager);
super.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
/*beforeInvocation做的事*/
//super.beforeInvocation(fi);源码
//获取请求资源的权限
//执行Collection attributes =
SecurityMetadataSource.getAttributes(object); //object为FilterInvocation对象
//是否拥有权限;authentication:封装了用户拥有的权限
//this.accessDecisionManager.decide(authenticated, object, attributes);
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
@Override
public void destroy() {
}
@Override
public Class extends Object> getSecureObjectClass() {
//下面的MyAccessDecisionManager的supports方法必须返回true,否则会提醒类型错误
return FilterInvocation.class;
}
}
自定义过滤器需要用到的用户是否拥有所请求资源的权限:MyAccessDecisionManager。默认使用AbstractAccessDecisionManager,具体是继承了AbstractAccessDecisionManager的AffirmativeBased,还有其他其他的决策管理器,不过不知道怎么启用,只是投票方式不同而已,有的是只要有赞成票就通过权限验证,有的是只要有反对票就不通过。但是看不懂底层是根据什么投赞成票和反对票的,反正不通过就抛AccessDenyException,一层层向上抛出,由ExceptionTranslationFilter打印异常日志。
package com.study.security;
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.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
/**
* 决定authentication拥有的权限中有没有指定资源的权限
* object:受保护对象,其可以是一个MethodInvocation、JoinPoint或FilterInvocation
* authentication:封装了用户拥有的权限
* configAttributes:受保护对象的相关配置属性,主要是访问资源需要的权限
*/
public void decide(Authentication authentication, Object object,
Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if(configAttributes == null) {
return;
}
//所请求的资源需要的权限(一个资源对多个权限)
Iterator iterator = configAttributes.iterator();
while(iterator.hasNext()) {
ConfigAttribute configAttribute = iterator.next();
//访问所请求资源所需要的权限
String Permission = configAttribute.getAttribute();
System.out.println("needPermission is " + Permission);
//用户所拥有的权限authentication
for(GrantedAuthority ga : authentication.getAuthorities()) {
if(Permission.equals(ga.getAuthority())) {
return;
}
}
}
//没有权限,后台抛异常而前端显示access-denied-page页面
throw new AccessDeniedException(" 没有权限访问或未重新登录! ");
}
/**
* 判断AccessDecisionManager是否能够处理对应的ConfigAttribute
*/
public boolean supports(ConfigAttribute attribute) {
// TODO Auto-generated method stub
return true;
}
/**
* 判断AccessDecisionManager是否支持对应的受保护对象类型
*/
public boolean supports(Class> clazz) {
// TODO Auto-generated method stub
return true;
}
}
自定义过滤器需要用到的获取资源权限(即查询t_resources表中所有resKey与resUrl的关系):MySecurityMetadataSource
package com.study.security;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
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.stereotype.Component;
import com.study.dao.ResourcesDao;
import com.study.model.Resources;
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Resource
private ResourcesDao resourcesDao;
private static Map> resourceMap = null;
public Collection getAllConfigAttributes() {
return null;
}
public boolean supports(Class> clazz) {
return true;
}
/**
* @PostConstruct是Java EE 5引入的注解,
* Spring允许开发者在受管Bean中使用它。当DI容器实例化当前受管Bean时,
* @PostConstruct注解的方法会被自动触发,从而完成一些初始化工作
*/
@PostConstruct
private void loadResourceDefine() { //加载所有资源与权限的关系
if (resourceMap == null) {
resourceMap = new HashMap>();
//数据库查询所有资源
List list = resourcesDao.queryAll(new Resources());
//Resources:本项目自定义的实体
for (Resources resources : list) {
Collection configAttributes = new ArrayList();
// 通过资源名称来表示具体的权限 注意:必须"ROLE_"开头
/*SecurityConfig:ConfigAttribute实现类;
只有一个成员,是个常量,构造函数传入string赋予此常量,
configAttribute.getAttribute()返回此常量*/
ConfigAttribute configAttribute = new SecurityConfig("ROLE_" + resources.getResKey());
configAttributes.add(configAttribute);
resourceMap.put(resources.getResUrl(), configAttributes);
}
}
}
//返回所请求资源所需要的权限
public Collection getAttributes(Object object) throws IllegalArgumentException {
//从过滤链中获取当前请求
String requestUrl = ((FilterInvocation) object).getRequestUrl();
//如果资源角色库为空的话说明还未初始化
if(resourceMap == null) {
loadResourceDefine(); //强制初始化
}
//System.err.println("resourceMap.get(requestUrl); "+resourceMap.get(requestUrl));
//如果是get请求的话去掉后面参数
if(requestUrl.indexOf("?")>-1){
requestUrl=requestUrl.substring(0,requestUrl.indexOf("?"));
}
//获取该请求对应权限,url作为key
Collection configAttributes = resourceMap.get(requestUrl);
return configAttributes;
}
}
ExceptionTranslationFilter处理AuthenticationException和AccessDeniedException两种异常。
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
//如果异常是AuthenticationException(未登录即访问受保护资源)
if (exception instanceof AuthenticationException) {
//打印异常
this.logger.debug("Authentication exception occurred;
redirecting to authentication entry point", exception);
/*传入此方法后主要是调用AuthenticationEntryPoint的默认实现类
LoginUrlAuthenticationEntryPoint中的commence方法重定向到登录页*/
this.sendStartAuthentication(request, response, chain,
(AuthenticationException)exception);
/* sendStartAuthentication源码
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication((Authentication)null);
this.requestCache.saveRequest(request, response);
this.logger.debug("Calling Authentication entry point.");
this.authenticationEntryPoint.commence(request, response, reason);
}
*/
}
//如果是AccessDeniedException
else if (exception instanceof AccessDeniedException) {
//且是匿名用户(未登录即访问受保护资源;一般是该异常,少为AuthenticationException)
if (this.authenticationTrustResolver.isAnonymous(SecurityContextHolder
.getContext().getAuthentication())) {
this.logger.debug("Access is denied (user is anonymous);
redirecting to authentication entry point", exception);
//重定向到登录页,且把AccessDeniedException包装成AuthenticationException
this.sendStartAuthentication(request, response, chain,
new InsufficientAuthenticationException("Full authentication is required
to access this resource"));
}
//非匿名用户(已登录但权限不足)
else {
this.logger.debug("Access is denied (user is not anonymous);
delegating to AccessDeniedHandler", exception);
//重定向到403页面;AccessDeniedHandler默认实现类:AccessDeniedHandlerImpl
this.accessDeniedHandler.handle(request, response
,(AccessDeniedException)exception);
}
}
}