SpringSecurity

一、简介:

1.1SpringSecurity 的核心功能:

  • Authentication:认证,用户登陆的验证(解决你是谁的问题)
  • Authorization:授权,授权系统资源的访问权限(解决你能干什么的问题)
  • 安全防护防止跨站请求session 攻击

1.2与shiro的对比:

Spring Security因为它的复杂,所以从功能的丰富性的角度更胜一筹:

  • Spring Security默认含有对OAuth2.0的支持,与Spring Social一起使用完成社交媒体登录也比较方便。shiro在这方面只能靠自己写代码实现。

二、HttpBaic模式与FormLogin模式登录认证

Spring Security的HttpBasic模式,比较简单,只是进行了通过携带Http的Header进行简单的登录验证,而且没有定制的登录页面,所以使用场景比较窄。

所以这里重点介绍FormLogin模式:

formLogin模式的三要素:

  • 登录认证逻辑
  • 资源访问控制规则,如:资源权限、角色权限
  • 用户角色权限

2.0配置文件

server:
  port: 8888
  servlet:
    session:
      timeout: 10s #Session的超时时间,默认最小1分钟,小于1分钟也是1分钟
      cookie:
        http-only: true # 浏览器脚本无法访问cookie 安全
        secure: false #必须用https才能发送cookie

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/jpadata?useUnicode=true&characterEncoding=UTF8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: root
  freemarker:
    cache: false # 缓存配置 开发阶段应该配置为false 因为经常会改
    suffix: .html # 模版后缀名 默认为ftl
    charset: UTF-8 # 文件编码
    template-loader-path: classpath:/templates/
  security:
    loginType: JSON
    user:
      name: admin
      password: admin


logging:
    config: classpath:log4j2-dev.xml

mybatis:
    configuration:
      mapUnderscoreToCamelCase: true

2.1SecurityConfig

package com.springSecurityDemo.basicserver.config;

import com.springSecurityDemo.basicserver.config.auth.MyAuthenticationFailureHandler;
import com.springSecurityDemo.basicserver.config.auth.MyAuthenticationSuccessHandler;
import com.springSecurityDemo.basicserver.config.auth.MyExpiredSessionStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;

@Configuration
public class SecurityConfig  extends WebSecurityConfigurerAdapter {

    @Resource
    MyAuthenticationSuccessHandler mySuthenticationSuccessHandler;

    @Resource
    MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    //重写Springsecuriry配置,以达到自定义的需求
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //httpBasic模式的设置 保护所有请求,作用不大,一般不用
        //因为很容易破解
//        http.httpBasic()
//                .and()
//                .authorizeRequests().anyRequest() //所有请求
//                .authenticated(); //必须登录后才能访问
//    }

        //formLogin模式
        //关闭跨站请求保护
        http.csrf().disable()
             .formLogin()
                //登陆逻辑配置:
                //登陆页面,
                .loginPage("/login.html")
                .usernameParameter("uname") //登录表单form中用户名输入框input的name名,不修改的话默认是username
                .passwordParameter("pword") //form中密码输入框input的name名,不修改的话默认是password
                //登录表单form中action的地址,也就是处理认证请求的路径
                .loginProcessingUrl("/login")  //拦截前端的登陆请求,交由SpringSecurity控制
                //.defaultSuccessUrl("/index") //登陆成功做页面跳转
                //.failureUrl("/login.html") //登陆失败页面
                .successHandler(mySuthenticationSuccessHandler) //自定义登陆成功处理结果与defaultSuccessUrl("/index")互相排斥
                .failureHandler(myAuthenticationFailureHandler) //自定义登陆失败逻辑
             .and()

                //权限校验规则
             .authorizeRequests()
                //login页面和login的url谁都可以访问
                .antMatchers("/login.html","/login").permitAll()
                .antMatchers("/biz1","/biz2") //需要对外暴露的资源路径
                    .hasAnyAuthority("ROLE_user","ROLE_admin")  //user角色和admin角色都可以访问
                //.antMatchers("/syslog","/sysuser")
                    //.hasAnyRole("admin")  //admin角色可以访问
                    //.hasAnyAuthority("ROLE_admin")
                .antMatchers("/syslog").hasAuthority("sys:log") //通过权限id控制权限,有该权限id才能访问
                .antMatchers("/sysuser").hasAuthority("sys:user") //自定义key值,通过key值进行访问
                .anyRequest().authenticated()

                //Session的管理
             .and().sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) //默认的也是需要的时候才会创建Session
                .invalidSessionUrl("/login.html") //Session超时登陆跳转的页面
                .sessionFixation().migrateSession() //每次Session复制一份,重新生成JSessionID,保障安全
                .maximumSessions(1) //最大当前只能有一个用户登陆
                .maxSessionsPreventsLogin(false) //允许再次登陆,之前登陆的会被踢掉
                .expiredSessionStrategy(new MyExpiredSessionStrategy()); //引入我们自定义的超过在线人数的回调函数

    }


    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        //静态配置用户
        auth.inMemoryAuthentication()
                .withUser("user")
                .password(passwordEncoder().encode("123456"))
                .roles("user")
                    .and()
                .withUser("admin")
                .password(passwordEncoder().encode("123456"))
                .authorities("sys:log","sys:user") //赋予资源id,放行其访问资源
                //.roles("admin")
                    .and()
                .passwordEncoder(passwordEncoder());//配置BCrypt加密
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(WebSecurity web) {
        //将项目中静态资源路径开放出来
        web.ignoring()
           .antMatchers( "/css/**", "/fonts/**", "/img/**", "/js/**");
    }

}

2.2自定义登陆成功拦截器

package com.springSecurityDemo.basicserver.config.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.springSecurityDemo.basicserver.config.exception.AjaxResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登陆成功结果处理
 */

//通常我们不会直接继承AuthenticationSuccessHandler
    //继承SavedRequestAwareAuthenticationSuccessHandler 能 跳转到登陆之前未登陆请求的页面
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Value("${spring.security.loginType}") //通过@value加载配置文件中的参数
    private String loginType;
    //JACKjson的 对象与json字符串转换类
    private static ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws ServletException, IOException {
        if(loginType.equalsIgnoreCase("JSON")){ //如果配置文件里是JSON,那么就用JSON方式做响应
            //JSon的数据格式进行数据相应
            response.setContentType("application/json;charset=UTF-8");
            //objectMapper.writeValueAsString 对象转换成字符串
            response.getWriter().write(objectMapper.writeValueAsString(
                    AjaxResponse.success("/index")
            ));
        }else{
            //跳转到登陆之前请求的页面(记录上一次登陆后的请求,如果登陆成功还会跳转到上一次跳转到的页面)
            super.onAuthenticationSuccess(request,response,authentication);
        }


    }
}

2.2自定义登陆失败拦截器

package com.springSecurityDemo.basicserver.config.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.springSecurityDemo.basicserver.config.exception.AjaxResponse;
import com.springSecurityDemo.basicserver.config.exception.CustomException;
import com.springSecurityDemo.basicserver.config.exception.CustomExceptionType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//SimpleUrlAuthenticationFailureHandler 登陆失败之后默认跳转到登陆页
@Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    @Value("${spring.security.loginType}")
    private String loginType;
    private static ObjectMapper objectMapper = new ObjectMapper();
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception)
            throws IOException, ServletException {
        if(loginType.equalsIgnoreCase("JSON")){
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(
                    AjaxResponse.error(new CustomException(
                            CustomExceptionType.USER_INPUT_ERROR,
                            "用户名或者密码输入错误!"))
            ));
        }else{
            //跳转到登陆页面
            super.onAuthenticationFailure(request,response,exception);
        }

    }
}

2.3自定义Session超时回调类

package com.springSecurityDemo.basicserver.config.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;

import javax.servlet.ServletException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 *SessionInformationExpiredStrategy
 * 实现接口里面的onExpiredSessionDetected方法,当Session超时或非法的时候就会回调
 *
 */
public class MyExpiredSessionStrategy implements SessionInformationExpiredStrategy {
    private static ObjectMapper objectMapper = new ObjectMapper();
    //当Session超时或非法的时候就会回调
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        Map<String,Object> map  = new HashMap<>();
        map.put("code",0);
        map.put("msg","您已经在另外一台电脑或浏览器登录,被迫下线!");
        //event将自定义错误提示写回(或跳转页面类似)
        event.getResponse().setContentType("application/json;charset=UTF-8");
        event.getResponse().getWriter().write(
                objectMapper.writeValueAsString(map)
        );

    }
}