1. 程式人生 > 資訊 >華為 FreeBuds Lipstick 耳機限量發售:獨特口紅設計,售價 249 歐元

華為 FreeBuds Lipstick 耳機限量發售:獨特口紅設計,售價 249 歐元

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

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

我們登入一下看看。SpringSecurity預設的使用者名稱為user,密碼是:

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

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

登入:

返回成功

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、資料庫

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

將許可權資訊改為admins試試

表示沒有許可權訪問。

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

hasAnyAuthority

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

設定hasAnyAuthority,其他不變

hasAnyAuthority("admins","manager")

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

使用者認證授權註解的使用

@Secured

要想使用許可權註解,需要啟用許可權註解功能。在啟動類上加上下面註解

@EnableGlobalMethodSecurity(securedEnabled = true)

​ 判斷是否具有某個角色,另外需要注意的是這裡匹配的字串需要新增字首“ROLE_”

使用前,要註釋掉配置檔案中的許可權配置資訊。

controller

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

將許可權資訊中,角色改為ROLE_XX,再次測試

@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" );

在許可權構造資訊中將menu:update改為menu:select再次進行測試。

@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();

}

點選退出

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

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

記住我功能

實現原理

具體實現

這個建立表語句在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

訪問測試

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進行比對,一樣的話就可以訪問,不一樣就不允許訪問。

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、使用者希望登入百度,訪問百度登入後的資源。而使用者可以選擇使用微信賬號進行登入,實際上是將授權認證的流程交由微信(獨立第三方)來進行擔保。

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

3、使用者選擇同意後,進入百度的流程。這時,百度會獲取使用者的微信身份資訊,與百度自己的一個註冊賬號進行繫結。繫結完成之後,就會用這個繫結後的賬號完成自己的登入流程。

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

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

OAuth2.0協議

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)