從零開始Spring Security (一)

摘要

本文將一步步從零開始,搭建並討論Spring Security原理
本文代碼能夠在 https://github.com/markfredch... 中找到。
後續每篇文章相關代碼會以分支的方式上傳。css

第一步

  • Hello World Rest APIgit

  • 默認Spring Security保護github

Maven配置

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Java Code

@SpringBootApplication
public class SpringSecurityDemoApplication {

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

@RestController
@RequestMapping("/api")
class DemoRestController {
    @GetMapping("/hello")
    public String greeting() {
        return "Hello World";
    }
}

Demo

好了, 如此簡單的代碼。一個最基本的Rest API /api/hello已經開發完成,並被Spring Security經過Basic Auth進行保護。web

➜  ~ curl http://localhost:8080/api/hello
{"timestamp":1489887539548,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/api/hello"}

➜  ~ curl http://user:6a3810c9-1ad7-4694-a617-0411fec20c60@localhost:8080/api/hello
Hello World

用戶名密碼在服務的啓動Log中找。spring

2017-03-19 09:30:31.492  INFO 76011 --- [  restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-03-19 09:30:31.571  INFO 76011 --- [  restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-03-19 09:30:31.959  INFO 76011 --- [  restartedMain] b.a.s.AuthenticationManagerConfiguration : 

Using default security password: 6a3810c9-1ad7-4694-a617-0411fec20c60

2017-03-19 09:30:32.047  INFO 76011 --- [  restartedMain] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: OrRequestMatcher [requestMatchers=[Ant [pattern='/css/**'], Ant [pattern='/js/**'], Ant [pattern='/images/**'], Ant [pattern='/webjars/**'], Ant [pattern='/**/favicon.ico'], Ant [pattern='/error']]], []
2017-03-19 09:30:32.234  INFO 76011 --- [  restartedMain] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: Ant [pattern='/h2-console/**'], [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@e89b69d, org.springframework.security.web.context.SecurityContextPersistenceFilter@1932f51e, org.springframework.security.web.header.HeaderWriterFilter@65c2ab9f, org.springframework.security.web.authentication.logout.LogoutFilter@3d247454, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@1e897c55, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@75a44063, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@31201444, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@160c32f9, org.springframework.security.web.session.SessionManagementFilter@507dd201, org.springframework.security.web.access.ExceptionTranslationFilter@21062771, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1f8069f6]

知識點

  • spring-boot-starter-security 加上此依賴以後,Spring Boot會對app對一些最簡單的安全配置。api

  • 默認Basic Auth的用戶密碼須要在app啓動的日誌尋找。安全

第二步

  • 使用InMemoryAuthentication自定義用戶訪問session

  • API權限控制app

  • API中獲取當前用戶信息curl

添加WebSecurityConfiguration

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password("admin-password")
                .roles("ADMIN")
                .and()
                .withUser("guest")
                .password("guest-password")
                .roles("GUEST");

    }
}

SecurityUtils

public class SecurityUtils {
    public static String getCurrentUser() {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        Authentication authentication = securityContext.getAuthentication();
        if (authentication != null && authentication.getPrincipal() instanceof User) {
            return ((User) authentication.getPrincipal()).getUsername();
        } else {
            return null;
        }
    }
}

API接口定義訪問權限

@RestController
@RequestMapping("/api")
class DemoRestController {
    @GetMapping("/hello")
    @PreAuthorize("hasRole('ADMIN')")
    public String greeting() {
        return "Hello " + SecurityUtils.getCurrentUser();
    }
}

Demo

➜  ~ curl http://admin:admin-password@localhost:8080/api/hello
Hello admin
➜  ~ curl http://guest:guest-password@localhost:8080/api/hello
{"timestamp":1489904173901,"status":403,"error":"Forbidden","exception":"org.springframework.security.access.AccessDeniedException","message":"不容許訪問","path":"/api/hello"}

知識點

  • 當配置EnableGlobalMethodSecurity(prePostEnabled = true)時,Spring Security會啓用方法權限控制。

  • @PreAuthorize("hasRole('ADMIN')")方法權限控制。@PreAuthorize使用Spring Expression Language來描述方法權限。本例子爲當用戶有ADMIN權限時,才能訪問。

  • 當用戶完成登陸以後,默認設置下用戶信息會以org.springframework.security.core.userdetails.User保存在SecurityContextHolder

第三步

  • 添加一個公共API,此API不須要進行Security保護。例如用戶註冊。

WebSecurityConfiguration

在WebSecurityConfiguration中添加如下方法。

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring()
            .antMatchers("/api/public/api");
}

DemoRestController

@RestController
@RequestMapping("/api")
class DemoRestController {
    @GetMapping("/hello")
    @PreAuthorize("hasRole('ADMIN')")
    public String greeting() {
        return "Hello " + SecurityUtils.getCurrentUser();
    }

    @GetMapping("/public/api")
    public String publicAPI() {
        return "this is public API";
    }
}

Demo

➜  ~ curl http://localhost:8080/api/public/api
this is public API