spring security4.x配置以及工作流程
一、资源过滤器WebSecurityConfig.java
/**
* @since 2017-11-08
* @author wwl
*
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true) //启用Security注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MySecurityFilter mySecurityFilter;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//http://localhost:8080/login 输入正确的用户名密码 并且选中remember-me 则登陆成功,转到 index页面
//再次访问index页面无需登录直接访问
//访问http://localhost:8080/home 不拦截,直接访问,
//访问http://localhost:8080/hello 需要登录验证后,且具备 “ADMIN”权限hasAuthority("ADMIN")才可以访问
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.addFilterAt(myUsernamePasswordAuthenticationFilter(), MyUsernamePasswordAuthenticationFilter.class).exceptionHandling();
//http.exceptionHandling().accessDeniedHandler(accessDeniedHandler()).and().authorizeRequests().anyRequest().authenticated().expressionHandler(webSecurityExpressionHandler());
http
.authorizeRequests()
.antMatchers("/mystatic/**").permitAll() //对静态资源放行(静态资源放在src/main/resources/static)
.antMatchers("/enterLoginPage").permitAll()//访问:/enterLoginPage 无需登录认证权限
.antMatchers("/userLogin").permitAll() //用户名和密码验证接口无需认证
.antMatchers("/enterForgetPasswordPage").permitAll() //忘记密码页面
.antMatchers("/loginByQQoauth,/AfterLoginRedirect").permitAll()//对某些资源请求进行放行
.antMatchers("/enterLoginPage").anonymous() //匿名用户可以访问
//.anyRequest().authenticated() //其他所有资源都需要认证,登陆后访问(不是登录状态不可以访问,这种方式不允许匿名访问)
.antMatchers("/enterAdminPage").hasAuthority("ROLE_ADMIN") //登陆后之后拥有“ROLE_ADMIN”权限才可以访问/hello方法,否则系统会出现“403”权限不足的提示
.and()
.formLogin()
.loginPage("/enterLoginPage")//指定登录页是”login.jsp”
//.failureForwardUrl("/enterLoginPage?loginerror=true") //登录失败后跳转的链接
.failureUrl("/enterLoginPage?loginerror=yes")
.usernameParameter("username").passwordParameter("password") //设置登录的用户名和密码参数
//.defaultSuccessUrl("/enterIndex", true)
.successHandler(loginSuccessHandler()) //登录成功后可使用loginSuccessHandler()存储用户信息,可选。
//.successForwardUrl("/enterIndex")
.permitAll()
.and()
.logout()
.logoutUrl("/loginout")
.logoutSuccessHandler(mylogoutSuccessHandler()) //成功退出的处理器
//.logoutSuccessUrl("/loginout") //退出登录后的默认网址是”/enterLoginPage” 又回到登录界面
.permitAll()
.invalidateHttpSession(true)
.and()
.rememberMe()//登录后记住用户,下次自动登录,数据库中必须存在名为persistent_logins的表
.tokenValiditySeconds(1209600) //rememberme保存的时间(大约保存14天)
.tokenRepository(tokenRepository()) //指定记住登录信息所使用的数据源
.userDetailsService(customUserDetailsService)
.and().csrf().disable(); //先关闭csrf验证
//当没有权限的时候,进入accessDeniedHandler
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler());
//只能允许同时一个人登录(另一个人登录同一个账号,会把之前的挤下去)
http.sessionManagement().maximumSessions(1).expiredUrl("/enterLoginPage");
http.addFilterBefore(mySecurityFilter, FilterSecurityInterceptor.class);//在正确的位置添加我们自定义的过滤器
}
@Override
public void configure(WebSecurity web) throws Exception {
//对静态资源和jsp页面放行
web.ignoring().antMatchers("/mystatic/**","/*.jsp","/**/*.jsp","/**/**/*.jsp","WEB-INF/jsp/**");
web.ignoring().antMatchers("/mystatic/**");
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//指定密码加密所使用的加密器为passwordEncoder()
//需要将密码加密后写入数据库 加密方法
//auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
auth.userDetailsService(customUserDetailsService); //先不加密
auth.authenticationProvider(authenticationProvider());
auth.eraseCredentials(false); //不删除凭据,以便记住用户
}
//注入数据源
@Autowired
private DataSource dataSource;
//spring security 内部都写死了,这里要把 这个DAO 注入
@Bean
public JdbcTokenRepositoryImpl tokenRepository(){
JdbcTokenRepositoryImpl j=new JdbcTokenRepositoryImpl();
j.setDataSource(dataSource);
return j;
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(customUserDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(4); //参数为加密的强度,越大说明强度越高,最大为10
}
@Bean
public LoginSuccessHandler loginSuccessHandler(){ //登录成功后的处理器
return new LoginSuccessHandler();
}
@Bean
public MylogoutSuccessHandler mylogoutSuccessHandler(){ //成功退出后的处理器
return new MylogoutSuccessHandler();
}
@Bean
public MyAccessDeniedHandler myAccessDeniedHandler(){ //没有对应权限的时候的处理器(必须要是登录后)
return new MyAccessDeniedHandler();
}
}
二、安全容器加载、拦截器MySecurityFilter.java
/**
* 该过滤器的主要作用就是通过spring著名的IoC生成securityMetadataSource。
* securityMetadataSource相当于本包中自定义的MyInvocationSecurityMetadataSourceService。
* 该MyInvocationSecurityMetadataSourceService的作用提从数据库提取权限和资源,装配到HashMap中,
* 供Spring Security使用,用于权限校验。
* @author wwl
* @since 2017.11.16
*
*/
@Component
public class MySecurityFilter
extends AbstractSecurityInterceptor
implements Filter{
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
private CustomAccessDecisionManager myAccessDecisionManager;
@Autowired
private AuthenticationManager authenticationManager;
@PostConstruct
public void init(){
super.setAuthenticationManager(authenticationManager);
super.setAccessDecisionManager(myAccessDecisionManager);
}
public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException{
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
User userDetails = null;
String username = null;
try{
userDetails = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
username = userDetails.getUsername();
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Iterator it = ((UserDetails)principal).getAuthorities().iterator();
String authority = "";
while(it.hasNext()){ //账户和角色是多对一关系,所以只迭代一次
authority = ((GrantedAuthority)it.next()).getAuthority();
}
req.getSession().setAttribute("role", authority); //角色名放入session中
}catch(Exception e){ //这里不能抛出异常
}
if(username != null && username != ""){
req.getSession().setAttribute("username", username);
}
FilterInvocation fi = new FilterInvocation( request, response, chain );
invoke(fi);
}
public Class<? extends Object> getSecureObjectClass(){
return FilterInvocation.class;
}
public void invoke( FilterInvocation fi ) throws IOException, ServletException{
//fi里面有一个被拦截的url
//里面调用CustomInvocationSecurityMetadataSourceService的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用CustomAccessDecisionManager的decide方法来校验用户的权限是否足够
//System.out.println("开始过滤请求...");
InterceptorStatusToken token = super.beforeInvocation(fi);
try{
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}finally{
super.afterInvocation(token, null);
}
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource(){
//System.out.println("加载资源...");
return this.securityMetadataSource;
}
public void destroy(){
System.out.println("filter===========================end");
}
public void init( FilterConfig filterconfig ) throws ServletException{
System.out.println("filter===========================begin");
}
}
三、登录、访问CustomInvocationSecurityMetadataSourceService.java
/**
* 最核心的地方,就是提供某个资源对应的权限定义,即getAttributes方法返回的结果。 此类在初始化时,应该取到所有资源及其对应角色的定义。
* @since 2017-11-08
* @author wwl
*/
@Service
public class CustomInvocationSecurityMetadataSourceService implements
FilterInvocationSecurityMetadataSource {
@Autowired //自动装配
private UserSecurityService userSecurityService;
private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
@PostConstruct//被@PostConstruct修饰的方法会在服务器加载Servle的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。
private void loadResourceDefine() {
if(resourceMap == null) {
resourceMap = new ConcurrentHashMap<String, Collection<ConfigAttribute>>();
}else{
resourceMap.clear();
}
// 在Web服务器启动时,提取系统中的所有权限。
//String为数据库中读取的URL
Map<String,List<Srole>> resourceRoleMap = userSecurityService.getAllResourceRole();
//依次取出所有角色名,加入到Authority集合中
for (Entry<String,List<Srole>> entry : resourceRoleMap.entrySet()) {
String url = entry.getKey(); //获取的url(本地数据库获取的url资源)
List<Srole> values = entry.getValue(); //获取该URL所具有的角色名
Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
for(Srole srole : values){ //角色名放入ConfigAttribute
ConfigAttribute configAttribute = new SecurityConfig(srole.getRolename()); //吧角色名称加入到配置中
configAttributes.add(configAttribute);
}
resourceMap.put(url, configAttributes);
}
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return new ArrayList<ConfigAttribute>();
}
// 根据URL,找到相关的权限配置。
@Override
public Collection<ConfigAttribute> getAttributes(Object object) //object是一个链接资源请求
throws IllegalArgumentException {
//System.out.println("进行了请求的拦截....");
// object 是一个URL,被用户请求的url。
FilterInvocation filterInvocation = (FilterInvocation) object;
//System.out.println("请求的链接资源:"+filterInvocation.getHttpRequest().getServletPath());
if (resourceMap == null) {
loadResourceDefine(); //从数据库中加载资源权限
}
Iterator<String> ite = resourceMap.keySet().iterator();
while (ite.hasNext()) { //拦截的URL和数据库中读取的URL进行匹配
String resURL = ite.next(); //resURL是数据库中读取的链接
//System.out.println("从数据库中读取的URL资源:"+resURL);
RequestMatcher requestMatcher = new AntPathRequestMatcher(resURL);
if(requestMatcher.matches(filterInvocation.getHttpRequest())) {
//System.out.println("匹配成功...");
return resourceMap.get(resURL); //如果匹配成功,那么则返回该URL对应的角色名集合(不止一个)
}
}
//System.out.println("没有匹配的资源...");
//如果数据库中没有相匹配的资源,那么返回空
return null;
}
@Override
public boolean supports(Class<?> arg0) {
return true;
}
}
四、获取用户、角色、资源、账号停用信息CustomUserDetailsService.java
/**
* @since 2017-11-09
* @author wwl
*
*/
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired //业务服务类
private UserSecurityService userService;
/**
* userName为输入的用户名
*/
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//SysUser对应数据库中的用户表,是最终存储用户和密码的表,可自定义
//根据用户名(userName)获取角色名称(rolename)
Map map ; //数据库中的字段类型不止一种,所以在这里不能使用泛型
map = userService.getUserRoleByUsernameService(userName); //根据用户名获取角色表和用户表
System.out.println("用户登录输入的用户名:"+userName);
if(map==null){
//System.out.println("从数据库中没有查到用户信息...");
throw new UsernameNotFoundException("member "+userName +" not found.");
}else{
System.out.println("map不为空...."+map.get("userpass"));
}
Collection<GrantedAuthority> authorities = new ArrayList<>();
//1:此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
authorities.add(new SimpleGrantedAuthority((String)map.get("rolename"))); //获取角色名称
User userdetail = new User((String)map.get("username"), (String)map.get("userpass"),
(Integer)map.get("enabled")==1?true:false, // 账号状态 0 表示停用 1表示启用
true, true, true, authorities // 用户的权限 (该用户包含的角色名称,本示例中用户和角色是一对一关系)
);
return userdetail;
}
}
五、访问资源决策器CustomAccessDecisionManager.java
/**
*AccessdecisionManager在Spring security中是很重要的。
*
*在验证部分简略提过了,所有的Authentication实现需要保存在一个GrantedAuthority对象数组中。
*这就是赋予给主体的权限。 GrantedAuthority对象通过AuthenticationManager
*保存到 Authentication对象里,然后从AccessDecisionManager读出来,进行授权判断。
*
*Spring Security提供了一些拦截器,来控制对安全对象的访问权限,例如方法调用或web请求。
*一个是否允许执行调用的预调用决定,是由AccessDecisionManager实现的。
*这个 AccessDecisionManager 被AbstractSecurityInterceptor调用,
*它用来作最终访问控制的决定。 这个AccessDecisionManager接口包含三个方法:
*
void decide(Authentication authentication, Object secureObject,
List config) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
从第一个方法可以看出来,AccessDecisionManager使用方法参数传递所有信息,这好像在认证评估时进行决定。
特别是,在真实的安全方法期望调用的时候,传递安全Object启用那些参数。
比如,让我们假设安全对象是一个MethodInvocation。
很容易为任何Customer参数查询MethodInvocation,
然后在AccessDecisionManager里实现一些有序的安全逻辑,来确认主体是否允许在那个客户上操作。
如果访问被拒绝,实现将抛出一个AccessDeniedException异常。
这个 supports(ConfigAttribute) 方法在启动的时候被
AbstractSecurityInterceptor调用,来决定AccessDecisionManager
是否可以执行传递ConfigAttribute。
supports(Class)方法被安全拦截器实现调用,
包含安全拦截器将显示的AccessDecisionManager支持安全对象的类型。
@since 2017-11-08
@author wwl
*/
@Service
public class CustomAccessDecisionManager implements AccessDecisionManager {
public void decide( Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException{
if( configAttributes == null ) { //说明之前请求的链接资源在数据中没有找到,所以getAttributes方法返回空
return ;
}
Iterator<ConfigAttribute> ite = configAttributes.iterator(); //是请求URL所对应的角色名集合,赋值给迭代器
while( ite.hasNext()){
ConfigAttribute ca = ite.next();
String needRole = ((SecurityConfig)ca).getAttribute(); //needRole为单个角色名
//ga 为用户所被赋予的权限。 needRole 为访问相应的资源应该具有的权限。
for( GrantedAuthority ga: authentication.getAuthorities()){
if(needRole.trim().equals(ga.getAuthority().trim())){
//System.out.println("成功匹配,可以访问该资源...");
return;
}
}
}
throw new AccessDeniedException("权限不足"); //权限不足,那么抛出异常
}
public boolean supports( ConfigAttribute attribute ){
return true;//都要设为true
}
public boolean supports(Class<?> clazz){
return true;//都要设为true
}
}
六、权限不足处理器MyAccessDeniedHandler.java
/**
* 请求资源没有对应权限的处理器
* @since 2017-11-24
* @author wwl
*
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private String errorPage = "hasNoRights";
@Override
@ResponseBody
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
//boolean isAjax = ControllerTools.isAjaxRequest(request);
if(request.getHeader("x-requested-with")!= null && request.getHeader("x-requested-with").equals("XMLHttpRequest")){
//是ajax请求
//System.out.println("该请求是ajax请求...不必跳转页面");
try {
String contentType = "application/json";
response.setContentType(contentType);
String jsonObject="{\"tip\":\"403\"}";
//System.out.println("通过ajax方法访问的资源,没有权限");
PrintWriter out = response.getWriter();
out.print(jsonObject);
out.flush();
out.close();
return ;
} catch (IOException e) {
e.printStackTrace();
}
}else if (!response.isCommitted()) {
if (errorPage != null) {
//System.out.println("不是ajax请求...那么跳转页面");
// Put exception into request scope (perhaps of use to a view)
request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
// 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, accessDeniedException.getMessage());
}
}
}
}
七、访问日志记录LoginSuccessHandler.java
/**
* 登录成功后的处理器,应该日志进行记录
* @since 2017-11-08
* @author wwl
*
*/
@Component
public class LoginSuccessHandler extends
SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private LogLogServiceImpl logLogServiceImpl;
@DateTimeFormat(pattern="yyyy-MM-dd hh:mm:ss")
Date date = new Date();
@Autowired
private UserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) throws IOException,
ServletException {
//获得授权后可得到用户信息 可使用SUserService进行数据库操作
User userDetails = (User)authentication.getPrincipal();
//输出登录提示信息
//System.out.println("用户" + userDetails.getUsername() + " 登录");
//登录客户端的IP地址(数据库记录登录日志信息)
//System.out.println("IP :"+request.getRemoteAddr()+"\t远程客户端主机名:"+request.getRemoteHost());
LogLog logLog = new LogLog(); //实例化日志bean
logLog.setUsername(userDetails.getUsername().trim());
logLog.setRemoteaddr(request.getRemoteAddr().trim());
logLog.setRemotehost(request.getRemoteHost().trim());
date = new Date();
logLog.setLogintime(date); //加入时间
logLog.setActions("登录".trim());
logLogServiceImpl.insertLogLogRecordsService(logLog);
// try{
// request.getRequestDispatcher(“enterIndex”).forward(request, response); //进入网站
// }catch(Exception e){
// System.out.println(“地址跳转时发生了异常”);
// }
//把用户名信息放入session中
Suser suser = new Suser();
suser.setUsername(userDetails.getUsername());
request.getSession().setAttribute(“user”, userService.getUserByUsernameService(suser.getUsername()));
this.setDefaultTargetUrl("/enterIndex");
this.setAlwaysUseDefaultTargetUrl(true); //必须要设置为true,设置url才能生效
super.onAuthenticationSuccess(request, response, authentication);
}
}
MylogoutSuccessHandler.java
/**
* @since 2017-11-25
* @author wwl
*
*/
@Component
public class MylogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private LogLogServiceImpl logLogServiceImpl;
@DateTimeFormat(pattern="yyyy-MM-dd hh:mm:ss")
Date date = new Date();
@Override
public void onLogoutSuccess
(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
//退出进行写日志操作
if(authentication != null){
User userDetails = (User)authentication.getPrincipal();
LogLog logLog = new LogLog(); //实例化日志bean
logLog.setUsername(userDetails.getUsername().trim());
logLog.setRemoteaddr(request.getRemoteAddr().trim());
logLog.setRemotehost(request.getRemoteHost().trim());
date = new Date();
logLog.setLogintime(date); //加入时间
logLog.setActions("退出".trim());
logLogServiceImpl.insertLogLogRecordsService(logLog);
}else{
//System.out.println("authentication is null");
}
// this.setDefaultTargetUrl("/loginout"); //设置的退出登录的链接
//this.setAlwaysUseDefaultTargetUrl(true); //设置为true
response.sendRedirect("enterLoginPage");
super.onLogoutSuccess(request, response, authentication);
}
}
八、异常处理器ExceptionReslove.java
/**
* 全局统一异常处理器
* @since 2017-12-12
* @author wwl
*
*/
@ControllerAdvice
public class ExceptionReslove {
private static final String ERROR = “error/500”;
private static final String COOKIEURL = "login";
/**
* 对于CookieTheftException异常处理
* @param request
* @param response
* @param modelAndView
* @return
*/
@ExceptionHandler({org.springframework.security.web.authentication.rememberme.CookieTheftException.class})
public ModelAndView CookieTheftExceptionHandler(HttpServletRequest request,HttpServletResponse response,
ModelAndView modelAndView){
//这里对CookieTheftException特殊处理
modelAndView.setViewName(this.COOKIEURL);
return modelAndView;
}
/**
* 全局异常处理器,可以根据status细处理
* @param modelAndView
* @return
*/
@ExceptionHandler({Exception.class})
public ModelAndView ExceptionResloveMethod(HttpServletRequest request,HttpServletResponse response,
ModelAndView modelAndView){
modelAndView.addObject("error", "ok");
modelAndView.setViewName(this.ERROR);
return modelAndView;
}
}
九、spring security4.x工作流程
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
主要有两个重要部分:
1.认证授权
用户登录,会被AuthenticationProcessingFilter拦截,调用AuthenticationManager的实现类,而且AuthenticationManager会调用ProviderManager获取用户验证信息,如果验证通过后会把用户的账户、角色放入spring security内部封装的userDetails的User对象中,最后userDetails会放入SecurityContextHolder安全容器中,用于后面用户访问资源进行权限鉴别。
2.访问授权
访问url时,会通过AbstractSecurityInterceptor拦截器拦截,其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限,在调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等)这里配置的是一票否决方式,如果权限足够,则返回,权限不够则报错并调用权限不足页面。
并且spring security提供了免登陆和日志记录功能(这里的日志记录是改造过的)