SpringSecurity


SpringSecurity

簡介

Spring Security 基於 Spring 框架,提供了一套 Web 應用安全性的完整解決方案。

關於安全方面的兩個主要區域是“認證”和“授權”(或者訪問控制),一般來說,Web 應用的安全性包括用戶認證(Authentication)和用戶授權(Authorization)兩個部分,這兩點也是 Spring Security 重要核心功能。

1、用戶認證:驗證某個用戶是否為系統中的合法主體,也就是說用戶能否訪問該系統。用戶認證一般要求用戶提供用戶名和密碼。系統通過校驗用戶名和密碼來完成認證過程。通俗點說就是系統認為用戶是否能登陸。

2、用戶授權:驗證某個用戶是否有權限執行某個操作。在一個系統中,不同用戶說具有的權限是不同的。比如對一個文件夾來說,有的用戶只能進行讀取,有的用戶可以進行修改。一般來說,系統不會為不同的用戶分配不同的角色,二每個角色則對應一些列的權限。通俗點說就是系統判斷用戶是否有權限去做某些事情。

SpringSecurity特點:

1、與Spring無縫整合

2、全面的權限控制

3、專門為web開發而設計

​ 3.1、舊版本不能脫離Web環境使用

​ 3.2、新版本對整個框架進行了分層抽取,分成了核心模塊和Web模塊。單獨引入核心模塊就可以脫離Web環境

4、重量級

入門案例

搭建基礎環境

采用SpringBoot+SpringSecurity的方式搭建項目

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1.RELEASE</version>
    <relativePath/>
</parent>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

controller

@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("hello")
    public String add(){
        return "hello security";
    }
}

config:主要是security的安全配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    public void Configure(HttpSecurity http) throws Exception {
        http.formLogin()//表單登陸
                .and()
                //認證配置
                .authorizeRequests()
                // 任何請求
                .anyRequest()
                // 都需要身份驗證
                .authenticated();
    }
}

yml:

server:
  port: 8080

測試

訪問http://localhost:8080/test/hello,他會自動跳轉到http://localhost:8080/login

image-20201108090259957

這樣,我們的Security初始配置就完成了。我嘗試把config注釋掉,然后繼續訪問test/hello,發現,還是能跳轉到login頁面,這是由於SpringBoot給我們做了一些自動配置。

我們登錄一下看看。SpringSecurity默認的用戶名為user,密碼是:

Using generated security password: 8f777589-cc2f-4649-a4d7-622ddaca7ae2

每次啟動都會生成一個密碼打印在控制台。

登錄:

image-20201108091143449

返回成功

SpringSecurity基本原理

SpringSecurity本質是一個過濾器鏈

項目啟動就可以加載過濾器鏈:

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter    
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor

我們下面就找幾個過濾器來看一下

FilterSecurityInterceptor:是一個方法級的權限過濾器,基本位於過濾器鏈的最底層

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
    private FilterInvocationSecurityMetadataSource securityMetadataSource;
    private boolean observeOncePerRequest = true;

    public FilterSecurityInterceptor() {
    }
	
    public void init(FilterConfig arg0) {
    }

    public void destroy() {
    }
	
    /**
    	真正的過濾方法
    */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        this.invoke(fi);
    }

    public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
        this.securityMetadataSource = newSource;
    }

    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } else {
            if (fi.getRequest() != null && this.observeOncePerRequest) {
                fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
            }
			//如果之前的過濾器做了放行操作,才會往下執行
            InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
                //執行本身的過濾器方法
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            } finally {
                super.finallyInvocation(token);
            }

            super.afterInvocation(token, (Object)null);
        }

    }

    public boolean isObserveOncePerRequest() {
        return this.observeOncePerRequest;
    }

    public void setObserveOncePerRequest(boolean observeOncePerRequest) {
        this.observeOncePerRequest = observeOncePerRequest;
    }
}

ExceptionTranslationFilter:是一個異常過濾器,用來處理在認證授權過程中拋出異常

public class ExceptionTranslationFilter extends GenericFilterBean {
    private AccessDeniedHandler accessDeniedHandler;
    private AuthenticationEntryPoint authenticationEntryPoint;
    private AuthenticationTrustResolver authenticationTrustResolver;
    private ThrowableAnalyzer throwableAnalyzer;
    private RequestCache requestCache;
    private final MessageSourceAccessor messages;

    public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) {
        this(authenticationEntryPoint, new HttpSessionRequestCache());
    }

    public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint, RequestCache requestCache) {
        this.accessDeniedHandler = new AccessDeniedHandlerImpl();
        this.authenticationTrustResolver = new AuthenticationTrustResolverImpl();
        this.throwableAnalyzer = new ExceptionTranslationFilter.DefaultThrowableAnalyzer();
        this.requestCache = new HttpSessionRequestCache();
        this.messages = SpringSecurityMessageSource.getAccessor();
        Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
        Assert.notNull(requestCache, "requestCache cannot be null");
        this.authenticationEntryPoint = authenticationEntryPoint;
        this.requestCache = requestCache;
    }

    public void afterPropertiesSet() {
        Assert.notNull(this.authenticationEntryPoint, "authenticationEntryPoint must be specified");
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;

        try {
            chain.doFilter(request, response);
            this.logger.debug("Chain processed normally");
        } catch (IOException var9) {
            throw var9;
        } catch (Exception var10) {
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10);
            RuntimeException ase = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (ase == null) {
                ase = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }

            if (ase == null) {
                if (var10 instanceof ServletException) {
                    throw (ServletException)var10;
                }

                if (var10 instanceof RuntimeException) {
                    throw (RuntimeException)var10;
                }

                throw new RuntimeException(var10);
            }

            if (response.isCommitted()) {
                throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var10);
            }

            this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
        }

    }

    public AuthenticationEntryPoint getAuthenticationEntryPoint() {
        return this.authenticationEntryPoint;
    }

    protected AuthenticationTrustResolver getAuthenticationTrustResolver() {
        return this.authenticationTrustResolver;
    }

    private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
        if (exception instanceof AuthenticationException) {
            this.logger.debug("Authentication exception occurred; redirecting to authentication entry point", exception);
            this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception);
        } else if (exception instanceof AccessDeniedException) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (!this.authenticationTrustResolver.isAnonymous(authentication) && !this.authenticationTrustResolver.isRememberMe(authentication)) {
                this.logger.debug("Access is denied (user is not anonymous); delegating to AccessDeniedHandler", exception);
                this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception);
            } else {
                this.logger.debug("Access is denied (user is " + (this.authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point", exception);
                this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource")));
            }
        }

    }

    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);
    }

    public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
        Assert.notNull(accessDeniedHandler, "AccessDeniedHandler required");
        this.accessDeniedHandler = accessDeniedHandler;
    }

    public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
        Assert.notNull(authenticationTrustResolver, "authenticationTrustResolver must not be null");
        this.authenticationTrustResolver = authenticationTrustResolver;
    }

    public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
        Assert.notNull(throwableAnalyzer, "throwableAnalyzer must not be null");
        this.throwableAnalyzer = throwableAnalyzer;
    }

    private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
        private DefaultThrowableAnalyzer() {
        }

        protected void initExtractorMap() {
            super.initExtractorMap();
            this.registerExtractor(ServletException.class, (throwable) -> {
                ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
                return ((ServletException)throwable).getRootCause();
            });
        }
    }
}

它判斷是哪個異常,然后針對不同異常進行處理handleSpringSecurityException。

UsernamePasswordAuthenticationFilter:對/login的POST請求做攔截,校驗表單中用戶名、密碼。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }
	
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }
}

attemptAuthentication方法:判斷是不是post提交,然后得到用戶名和密碼,然后進行校驗,當然,它用的是默認的用戶名和密碼來進行校驗的。實際開發中,這里要查詢數據庫來進行校驗。

我們查看了幾個過濾器,那么這些過濾器是如何加載的呢?下面就來看看。

過濾器是如何加載的

由於我們使用的是SpringBoot,它自動的配置了SpringSecurity的相關內容,本質上需要一些過程。SpringSecurity配置過濾器,這個過濾器叫做DelegatingFilterProxy,這些過程就是在這個過濾器中執行

DelegatingFilterProxy

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    //得到當前對象
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
        synchronized(this.delegateMonitor) {
            delegateToUse = this.delegate;
            if (delegateToUse == null) {
                WebApplicationContext wac = this.findWebApplicationContext();
                if (wac == null) {
                    throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
                }
				// 進行一系列的初始化后,調用初始化方法
                delegateToUse = this.initDelegate(wac);
            }

            this.delegate = delegateToUse;
        }
    }

    this.invokeDelegate(delegateToUse, request, response, filterChain);
}

initDelegate

protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
    String targetBeanName = this.getTargetBeanName();
    Assert.state(targetBeanName != null, "No target bean name set");
    // 從容器中獲取Filter,targetBeanName有一個固定的值FilterChainProxy
    Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
    if (this.isTargetFilterLifecycle()) {
        delegate.init(this.getFilterConfig());
    }

    return delegate;
}

FilterChainProxy:主要是將所有過濾器加載到過濾器鏈中。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
    if (clearContext) {
        try {
            request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
            
            this.doFilterInternal(request, response, chain);
        } finally {
            SecurityContextHolder.clearContext();
            request.removeAttribute(FILTER_APPLIED);
        }
    } else {
        this.doFilterInternal(request, response, chain);
    }

}

doFilterInternal:

private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    FirewalledRequest fwRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
    HttpServletResponse fwResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
    //加載過濾器中的所有過濾器
    List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);
    if (filters != null && filters.size() != 0) {
        FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);
        vfc.doFilter(fwRequest, fwResponse);
    } else {
        if (logger.isDebugEnabled()) {
            logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list"));
        }

        fwRequest.reset();
        chain.doFilter(fwRequest, fwResponse);
    }
}

總結:

流程:

​ 1、配置DelegatingFilterProxy

​ 2、調用DelegatingFilterProxy中的doFilter方法,這個方法中調用了initDelegate

​ 3、通過targetBeanName從容器中拿到所有的過濾器,targetBeanName有一個固定的值FilterChainProxy。

​ 4、掉用FilterChainProxy中的doFilter方法,這個方法中調用doFilterInternal方法,doFilterInternal會獲取到所有的過濾器,並執行這些過濾器

SpringSecurity兩個重要的接口

UserDetailsService接口

當什么也沒有配置的時候,帳號和密碼是由SPring Security定義生成的,而在實際開發中賬戶和密碼都是從數據庫中查詢出來,所以要通過自定義邏輯控制認證邏輯。

如果要自定義邏輯時,只需實現UserDetailsService接口即可。接口如下:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

返回值:UserDetails

方法參數:username

如果我們要自己去查數據庫,然后認證,需要我們去繼承UsernamePasswordAuthenticationFilter,然后實現attemptAuthentication和UsernamePasswordAuthenticationFilter父類的successfulAuthenticationunsuccessfulAuthentication方法,在attemptAuthentication方法中得到用戶名和密碼,如果認證成功,就會調用successfulAuthentication,不成功就調用unsuccessfulAuthentication。從數據庫中獲取密碼的操作是在UserDetailsService中完成的,這個方法會返回一個User對象,這個對象是由Security提供的。

PasswordEncoder接口

數據加密接口,用於返回User對象里面密碼的加密

用戶認證案例

web權限控制方案:

1、認證:

就是通過用戶名和密碼進行登錄的過程。

要想登錄,就需要設置用戶名和密碼。而設置用戶名和密碼有三種方式

第一種、通過配置文件配置

spring:
  security:
    user:
      name: zhangsan
      password: 123456

第二種、通過配置類

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder bcp = new BCryptPasswordEncoder();
        //將密碼進行加密
        String password = bcp.encode( "123456" );
        //認證信息加載到內存中
        auth.inMemoryAuthentication()
                //添加用戶名
                .withUser( "zhangsan" )
                //密碼
                .password( password )
                //角色
                .roles( "Admin" );
    }

    /**
     * 加密時需要用到PasswordEncoder接口,所以需要在容器中配置PasswordEncoder,
     * 這里我們創建PasswordEncoder的實現類BCryptPasswordEncoder。
     * 如果不設置會報錯There is no PasswordEncoder mapped for the id "null"
     * @return
     */
    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

第三種、自定義實現類

1、創建配置類,設置使用哪個userDetailsService實現類

@Configuration
public class SecurityConfigDetails extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService( userDetailsService )
                .passwordEncoder( getPasswordEncoder() );
    }

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

2、編寫實現類,返回User對象,User對愛過你有用戶名、密碼、角色信息。

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //構造權限信息
        List<GrantedAuthority> auths
                    = AuthorityUtils.commaSeparatedStringToAuthorityList( "" );
        //這里賬戶和密碼可以通過數據庫查出
        return new User( "zhangsan", new BCryptPasswordEncoder().encode( "123456" ), auths);
    }
}

整合Mybatis-plus測試

1、導入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

2、數據庫

image-20201108115105220

3、實體類

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Users {
    private Integer id;
    private String userName;
    private String password;
}

4、整合MybatisPlus

@Repository
public interface UserMapper extends BaseMapper<Users> {
}

5、SecurityConfigDetails中調用mapper里面的方法查詢數據庫

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        // 根據用戶名查詢用戶信息
        QueryWrapper<Users> wrapper = new QueryWrapper<Users>();
        wrapper.eq( "userName", userName );

        Users users = userMapper.selectOne( wrapper );
        if (users == null) {
            throw new UsernameNotFoundException( "用戶名找不到!" );
        }
        //構造權限信息
        List<GrantedAuthority> auths
                = AuthorityUtils.commaSeparatedStringToAuthorityList( "" );
        //從數據庫返回users對象,得到用戶名和密碼,返回
        return new User( users.getUserName(), new BCryptPasswordEncoder().encode( users.getPassword() ), auths );
    }
}

6、yml

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.101.128:3306/springsecurity?useSSL=false
    username: root
    password: 123456

7、啟動類

@SpringBootApplication
@MapperScan("com.ybl.securitydemo1.mapper")
public class Securitydemo1Application {

    public static void main(String[] args) {
        SpringApplication.run( Securitydemo1Application.class, args );
    }

}

自定義登錄頁面

在config中添加一下代碼

@Override
protected void configure(HttpSecurity http) throws Exception {
    //跳轉到自定義的登錄頁面
    http.formLogin()
            //登錄頁面地址
            .loginPage( "/login.html" )
            //登錄訪問路徑
            .loginProcessingUrl( "/user/login" )
            //登錄成功后跳轉路徑
            .defaultSuccessUrl( "/test/index" ).permitAll()
            //表示訪問下面這些路徑的時候不需要認證
            .and().authorizeRequests()
                .antMatchers( "/","/test/hello","/user/login" ).permitAll()
            .anyRequest().authenticated()
            //關閉csrf防護
            .and().csrf().disable();

}

html

<!DOCTYPE html>
<html lang="zh">
<head>
    
    <title>login</title>
</head>
<body>
    <form action="/user/login" method="post">
        用戶名:<input name="username" type="text"/>
        <br/>
        密碼:  <input name="password" type="password"/>
        <input type="submit" value="login">
    </form>
</body>
</html>

controller

@GetMapping("/index")
public String index(){
    return "hello index";
}

2、授權:

基於角色或權限進行訪問控制

hasAuthority:如果當前的主體具有指定的權限,返回true,沒有返回false

1、在配置類中設置當前訪問地址需要哪些權限

http.formLogin()
    .loginPage( "/login.html" )
    .loginProcessingUrl( "/user/login" )
    .defaultSuccessUrl( "/test/index" ).permitAll()
    .and().authorizeRequests()
    .antMatchers( "/","/test/hello","/user/login" ).permitAll()
    //當前用戶,只有具有amdins權限才可以訪問這個路徑
    .antMatchers( "/test/index" ).hasAuthority("admins")
    .and().csrf().disable();

2、在userDetailsServcer中在User對象中設置權限

@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
    // 根據用戶名查詢用戶信息
    QueryWrapper<Users> wrapper = new QueryWrapper<Users>();
    wrapper.eq( "userName", userName );

    Users users = userMapper.selectOne( wrapper );
    if (users == null) {
        throw new UsernameNotFoundException( "用戶名找不到!" );
    }
    //構造權限信息
    List<GrantedAuthority> auths
            = AuthorityUtils.commaSeparatedStringToAuthorityList( "admins" );
    //從數據庫返回users對象,得到用戶名和密碼,返回
    return new User( users.getUsername(), new BCryptPasswordEncoder().encode( users.getPassword() ), auths );
}

image-20201108213530264

將權限信息改為admins試試

image-20201108213645340

表示沒有權限訪問。

​ hasAuthority只能有一個權限,如果設置的是兩個或多個權限,也是不能訪問的。如hasAuthority("admins","manager")這種就是不能訪問的。這就需要hasAnyAuthority這種方式來設置了。

hasAnyAuthority

如果當前的主體有任何提供的角色(給定的作為一個逗號分割的字符串列表)的話,返回true。

設置hasAnyAuthority,其他不變

hasAnyAuthority("admins","manager")

image-20201108214349418

hasRole

​ 如果用戶具備給定角色就允許訪問,否則出現403,有指定角色,返回true

​ 底層源碼

private static String hasRole(String role) {
    Assert.notNull(role, "role cannot be null");
    if (role.startsWith("ROLE_")) {
        throw new IllegalArgumentException(
            "role should not start with 'ROLE_' since it is automatically inserted. Got '"
            + role + "'");
    }
    return "hasRole('ROLE_" + role + "')";
}

所以在構造權限的時候,需要在角色前面拼接上ROLE_.

//構造權限信息
List<GrantedAuthority> auths
    = AuthorityUtils.commaSeparatedStringToAuthorityList( "admins,ROLE_sale" );
http.formLogin()
    //登錄頁面地址
    .loginPage( "/login.html" )
    //登錄訪問路徑
    .loginProcessingUrl( "/user/login" )
    //登錄成功后跳轉路徑
    .defaultSuccessUrl( "/test/index" ).permitAll()
    //表示訪問下面這些路徑的時候不需要認證
    .and().authorizeRequests()
    .antMatchers( "/","/test/hello","/user/login" ).permitAll()
    //設置角色
    .antMatchers( "/test/index" ).hasRole("sale")
    //關閉csrf防護
    .and().csrf().disable();

hasAnyRole

這個和上面類似,用於多個角色的認證。

3、自定義403頁面

config:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.exceptionHandling().accessDeniedPage( "/unauth.html" );
    /**省略*
}

unauth.html

<!DOCTYPE html>
<html lang="zh">
<head>
    
    <title>Title</title>
</head>
<body>
    <h1>沒有權限,請聯系管理員</h1>
</body>
</html>

MyUserDetailsService

List<GrantedAuthority> auths
        = AuthorityUtils.commaSeparatedStringToAuthorityList( "ROLE_GG" );

image-20201108220037378

用戶認證授權注解的使用

@Secured

要想使用權限注解,需要啟用權限注解功能。在啟動類上加上下面注解

@EnableGlobalMethodSecurity(securedEnabled = true)

​ 判斷是否具有某個角色,另外需要注意的是這里匹配的字符串需要添加前綴“ROLE_”

使用前,要注釋掉配置文件中的權限配置信息。

controller

@GetMapping("/auth")
@Secured( "ROLE_GG" )
public String auth(){
    return "hello auth";
}

image-20201108221452910

將權限信息中,角色改為ROLE_XX,再次測試

image-20201108221615979

@PreAuthorize

開啟注解功能

@EnableGlobalMethodSecurity(prePostEnabled = true)

這個注解適合進入方法前的權限驗證,@PreAuthorize可以將登錄用戶的roles/permissions參數傳遞到方法中

//這種表示具有某個權限
@PreAuthorize( "hasAnyAuthority('menu:update')" )
//這中表示具有某個角色
//@PreAuthorize( "hasRole('ROLE_admin')" )
public String auth(){
    return "hello auth";
}
List<GrantedAuthority> auths
        = AuthorityUtils.commaSeparatedStringToAuthorityList( "menu:update" );

image-20201108222240313

在權限構造信息中將menu:update改為menu:select再次進行測試。

image-20201108222407140

@PostAuthorize

@EnableGlobalMethodSecurity(prePostEnabled = true)

這個注解使用不多,在方法執行后在進行權限驗證,適合驗證帶有返回值的權限。

@PostFilter

​ 權限驗證之后對數據進行過濾,留下用戶名是admin的數據

​ 表達式中的filterObject引用的是方法返回值List中的某一個元素

例子:

@PostFilter("filterObject.username='admin1'")
public List<UserInfo> getAllUser(){
   List<UserInfo> list = new ArrayList<>();
   list.add(new UserInfo(1,"admin1","666"));
   list.add(new UserInfo(2,"admin2","777"));\
   return list
}

這種情況下,返回的list中admin1着跳數據不會返回

對返回數據做過濾

@PreFilter

這個就是對請求參數做過濾

@PreFilter(value="filterObject.id%2==0")
public List<UserInfo> getAllUser(@RequestBody List<UserInfo> list){
   list.forEach(t->{
      System.out.println(t.getId()+"\t"+t.getUsername()); 
   });
}

這里只會打印能被2整除的UserInfo

用戶注銷

success.html

<!DOCTYPE html>
<html lang="zh">
<head>
    
    <title>Title</title>
</head>
<body>
login success<br/>
    <a href="/logout">退出</a>
</body>
</html>

config

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.exceptionHandling().accessDeniedPage( "/unauth.html" );
    //跳轉到自定義的登錄頁面
    http.formLogin()
        //登錄頁面地址
        .loginPage( "/login.html" )
        //登錄訪問路徑
        .loginProcessingUrl( "/user/login" )
        //登錄成功后跳轉路徑
        .defaultSuccessUrl( "/success.html" ).permitAll()
        //表示訪問下面這些路徑的時候不需要認證
        .and().authorizeRequests()
        .antMatchers( "/","/test/hello","/user/login" ).permitAll()
        //當前用戶,只有具有amdins權限才可以訪問這個路徑
        //.antMatchers( "/test/index" ).hasAuthority("admins")
        //.antMatchers( "/test/index" ).hasAnyAuthority("admins","manager")
        //
        //.antMatchers( "/test/index" ).hasRole("sale")
        //關閉csrf防護
        .and().csrf().disable();

    http.logout().logoutUrl( "/logout" ).logoutSuccessUrl( "/test/hello" ).permitAll();

}

image-20201108224531109

點擊退出

image-20201108224644681

然后訪問test/index,發現可以訪問

image-20201108225116122

訪問test/auth,發現回到了登錄頁面

記住我功能

實現原理

image-20201108230618611

image-20201108231246039

具體實現

這個創建表語句在org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl中

創建數據庫表:

CREATE TABLE persistent_logins (
	`username` VARCHAR(64) NOT NULL,
	`series` VARCHAR(64) PRIMARY KEY,
	`token` VARCHAR(64) NOT NULL,
	`last_used` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)ENGINE=INNODB DEFAULT CHARSET=utf8;

修改配置類

@Autowired
private DataSource dataSource;

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
    tokenRepository.setDataSource(dataSource);
    //這個方法是自動創建數據庫,我們自己創建了,這里就不用創建了
    //tokenRepository.setCreateTableOnStartup( true );
    return tokenRepository;
}

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling().accessDeniedPage( "/unauth.html" );
        http.formLogin()
                .loginPage( "/login.html" )
                .loginProcessingUrl( "/user/login" )
                .defaultSuccessUrl( "/success.html" ).permitAll()
                .and().authorizeRequests()
                .antMatchers( "/", "/test/hello", "/user/login" ).permitAll()
            	//記住我功能
                .and().rememberMe().tokenRepository( persistentTokenRepository() )
                //設置有效時長
                .tokenValiditySeconds( 60 )
                .userDetailsService(userDetailsService)
                .and().csrf().disable();

        http.logout().logoutUrl( "/logout" ).logoutSuccessUrl( "/test/hello" ).permitAll();
    }

在登錄頁面添加一個復選框,勾選表示需要自動登錄功能,不勾選表示不需要

<!DOCTYPE html>
<html lang="zh">
<head>
    
    <title>login</title>
</head>
<body>
    <form action="/user/login" method="post">
        用戶名:<input name="username" type="text"/>
        <br/>
        密碼:  <input name="password" type="password"/>
        <input type="submit" value="login">
        <br/>
        <input type="checkbox" name="remember-me"/>自動登錄
    </form>
</body>
</html>

checkbox的name必須為remeber-me

訪問測試

image-20201109000710264

image-20201109000722776

CSRF理解

​ 跨站請求偽造(Cross-site request forgery),也被稱為one-click attack或者sessionriding,通常寫為CSRF或者XSRF,是一種挾制用戶在當前已登錄的Web應用程序上執行非本意的操作的攻擊方式。跟跨站腳本(XSS)相比,XSS利用的是用戶對指定網站的信任,CSRF利用的是網站對用戶網頁瀏覽器的信任。

​ 跨站請求攻擊,簡單地說,是攻擊者通過一些技術手段欺騙用戶的瀏覽器去訪問自己曾經認證過的網站並運行一些操作(如發郵件、信息、甚至財產操作如轉賬和購買商品)。由於瀏覽器曾經認證過,所以被訪問的網站會認為是真正的用戶操作而去運行。這利用了web中用戶身份驗證的一個漏洞:簡單的身份驗證只能保證請求發自某個用戶的瀏覽器,缺不能保證請求本身是用戶自願發出的。

​ SpringSecurity 4.0開始,默認情況下會啟用CSRF保護,以防止CSRF攻擊應用程序,SpringSecurity CSRF會針對PATCH、POST、PUT和DELETE方法進行保護,對get請求不會保護。

SpringSecurity 怎么開啟呢?

我們先把.and().csrf().disable();這行代碼注釋掉就行,SpringSecurity默認是開啟的。然后在需要PATCH、POST、PUT和DELETE的請求的form表單中加上一個隱藏域就行。

隱藏域:

<input type="hidden" th:name="${_csrf.parmameterName}" th:value="${_csrf.token}}"/>

這里th:name 是用的是thymeleaf模板的標簽。要想使用thymeleaf模板引擎,需要在html標簽上添加如下代碼

<html lang="zh" xmlns:th="http://www.thymeleaf.org">

導入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

CSRF原理

1、生成csrfToken保存到HttpSession或者Cookie中

2、再次訪問的時候,需要攜帶token到后台和session存儲的token進行比對,一樣的話就可以訪問,不一樣就不允許訪問。

image-20201109003159597

csrf使用CSRFFilter過濾器實現的

Oauth2.0

什么是OAuth2.0?

OAuth(開放授權)是一個開放標准,允許用戶授權第三方應用訪問他們存儲在另外的服務提供者上的信息,而不需要將用戶名和密碼供給第三方應用或分享他們數據的所有內容。OAuh2.0是 OAuth協議的延續版本,但不向后兼容 OAuth1.0即完全廢止了 OAuth1.0。很大公司如 Google, Yahoo, Microsoft等都提供了 OAuh認證服務,這些都足以說明 OAuh標准逐漸成為開放資源授權的標准。

參考:https://baike.baidu.com/item/oAuth/7153134?rf=aladdin

Oauth協議:https://tools.ietf.org/html/rfc6749

OAuth2.0流程示例

OAuth認證流程,簡單理解,就是允許我們將之前實現的認證和授權的過程交由一個獨立的第三方來進行擔保。而OAuth協議就是用來定義如何讓這個第三方的擔保有效且雙方可信。例如我們下面用戶訪問百度登錄后的資源為例:

1、用戶希望登錄百度,訪問百度登錄后的資源。而用戶可以選擇使用微信賬號進行登錄,實際上是將授權認證的流程交由微信(獨立第三方)來進行擔保。

image-20210719215255901

2、用戶掃描二維碼的方式,在微信完成登錄認證

image-20210719215416860

3、用戶選擇同意后,進入百度的流程。這時,百度會獲取用戶的微信身份信息,與百度自己的一個注冊賬號進行綁定。綁定完成之后,就會用這個綁定后的賬號完成自己的登錄流程。

image-20210719215624007

以上這個過程,實際上就是一個典型的 OAuth2.0的認證流程。在這個登錄認證的過程中,實際上是只有用戶和百度之間有資源訪問的關系,而微信就是作為一個獨立的第三方,使用用戶在微信里的身份信息,來對用戶的身份進行了一次擔保認證。認證完成后,百度就可以獲取到用戶的微信身份信息,進入自己的后續流程,與百度內部的一個用戶信息完成綁定及登錄。整個流程大致是這樣

image-20210719215806806

整個過程,最重要的問題就是如何讓用戶、百度和微信這三方實現權限認證的共信。這其中涉及到許多的細節,而AOuth2.0協議就是用來定義這個過程中,各方的行為標准。

OAuth2.0協議

img

OAuth2.0協議包含了一下幾個角色:

1、客戶端——如瀏覽器、微信客戶端

本身不存儲資源,需要通過資源擁有者的授權去請求資源服務器服務器的資源。

2、資源擁有者——用戶(擁有微信賬號)

通常是用戶,也可以是應用程序,即該資源的擁有者

3、授權服務器(也稱為認證服務器)——示例中的微信

用於服務提供者對資源擁有的身份進行認證,對訪問資源進行授權,認證成功后會給客戶端發令牌(access_toke),作為客戶端訪問資源服務的憑證。

4、資源服務器——示例中的微信和百度

存儲資源的服務器。本示例中,微信通過OAuth協議讓百度可以獲取到自己存儲的用戶信息,而百度則通過OAuth協議,讓用戶可以訪問自己的受保護的資源。

這其中有幾個重要的概念:

clientDetails(client id):客戶信息。代表百度在微信中的唯一索引。在微信中用 appid區分 userDetails
secret:秘鑰。代表百度獲取微信信息需要提供的一個加密字段。這跟微信采用的加密算法有關。
scope:授權作用域。代表百度可以獲取到的微信的信息范圍。例如登錄范圍的憑證無法獲取用戶信息范圍的信息。
access_token:授權碼。百度獲取微信用戶信息的憑證。微信中叫做接口調用憑證。
grant_type:授權類型。例如微信目前僅支持基於授權碼的 authorization_code模式。而OAth2.0還可以有其他的授權方式,例如輸入微信的用戶名和密碼的方式。

userDetails(user_id):授權用戶標識。在示例中代表用戶的微信號。在微信中用 openid區分

關於微信登錄的功能介紹,可以查看微信的官方文檔:微信開放文檔 (qq.com)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM