Hello World

依照惯例,我们从一个 Hello World 项目入手。

我们新建了一个名为hello-world的 Gradle 项目。

基于Form 表单的登录认证。

环境

  • Gradle 3.4.1
  • Spring Boot 1.5.2.RELEASE
  • Thymeleaf 3.0.3.RELEASE
  • Thymeleaf Layout Dialect 2.2.0

Gradle Wrapper

修改 Gradle Wrapper 的配置 gradle-wrapper.properties,使用最新的Gradle:

  1. distributionBase=GRADLE_USER_HOME
  2. distributionPath=wrapper/dists
  3. zipStoreBase=GRADLE_USER_HOME
  4. zipStorePath=wrapper/dists
  5. distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip

build.gradle

1. 自定义依赖的版本

我们可以自定义 Spring Boot 的版本,比如,我们使用了目前最新的 1.5.2.RELEASE 版本。

  1. // buildscript 代码块中脚本优先执行
  2. buildscript {
  3. ......
  4. ext {
  5. springBootVersion = '1.5.2.RELEASE'
  6. }
  7. // 自定义 Thymeleaf 和 Thymeleaf Layout Dialect 的版本
  8. ext['thymeleaf.version'] = '3.0.3.RELEASE'
  9. ext['thymeleaf-layout-dialect.version'] = '2.2.0'
  10. ......
  11. }

2. 修改项目的名称

修改 build.gradle 文件,让我们的hello-world项目成为一个新的项目。

修改内容也比较简单,修改项目名称及版本即可。

  1. jar {
  2. baseName = 'hello-world'
  3. version = '1.0.0'
  4. }

3. 修改项目的仓库地址

为了加快构建速度,我们自定义了一个国内的仓库镜像地址:

  1. repositories {
  2. maven {
  3. url 'http://maven.aliyun.com/nexus/content/groups/public/'
  4. }
  5. }

4. 指定依赖

  1. // 依赖关系
  2. dependencies {
  3. // 该依赖对于编译发行是必须的
  4. compile('org.springframework.boot:spring-boot-starter-web')
  5. // 添加 Thymeleaf 的依赖
  6. compile('org.springframework.boot:spring-boot-starter-thymeleaf')
  7. // 添加 Spring Security 依赖
  8. compile('org.springframework.boot:spring-boot-starter-security')
  9. // 添加 Thymeleaf Spring Security 依赖,与 Thymeleaf 版本一致都是 3.x
  10. compile('org.thymeleaf.extras:thymeleaf-extras-springsecurity4:3.0.2.RELEASE')
  11. // 该依赖对于编译测试是必须的,默认包含编译产品依赖和编译时依
  12. testCompile('org.springframework.boot:spring-boot-starter-test')
  13. }

配置类

  1. @EnableWebSecurity
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. ......
  4. /**
  5. * 自定义配置
  6. */
  7. @Override
  8. protected void configure(HttpSecurity http) throws Exception {
  9. http
  10. .authorizeRequests()
  11. .antMatchers("/css/**", "/js/**", "/fonts/**", "/index").permitAll() // 虽都可以访问
  12. .antMatchers("/users/**").hasRole("USER") // 需要相应的角色才能访问
  13. .antMatchers("/admins/**").hasRole("ADMIN") // 需要相应的角色才能访问
  14. .and()
  15. .formLogin() //基于 Form 表单登录验证
  16. .loginPage("/login").failureUrl("/login-error"); // 自定义登录界面
  17. }
  18. ......
  19. }

这段代码内容很少,但事实上已经做了很多的默认安全验证,包括:

Hello World - 图1

那么,上述安全设置是如何默认启用的呢?我们观察下 SecurityConfig 所继承的 org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter类,其初始化的时候是这样的

  1. http
  2. .csrf().and()
  3. .addFilter(new WebAsyncManagerIntegrationFilter())
  4. .exceptionHandling().and()
  5. .headers().and()
  6. .sessionManagement().and()
  7. .securityContext().and()
  8. .requestCache().and()
  9. .anonymous().and()
  10. .servletApi().and()
  11. .apply(new DefaultLoginPageConfigurer<HttpSecurity>()).and()
  12. .logout();

也就是说,它默认的时候就隐式的启用了多安全设置。

再看下其他几个方法:

  1. /**
  2. * 用户信息服务
  3. */
  4. @Bean
  5. public UserDetailsService userDetailsService() {
  6. InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); // 在内存中存放用户信息
  7. manager.createUser(User.withUsername("waylau").password("123456").roles("USER").build());
  8. manager.createUser(User.withUsername("admin").password("123456").roles("USER","ADMIN").build());
  9. return manager;
  10. }
  11. /**
  12. * 认证信息管理
  13. * @param auth
  14. * @throws Exception
  15. */
  16. @Autowired
  17. public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
  18. auth.userDetailsService(userDetailsService());
  19. }

其含义为:

  • 用户的认证信息是使用 InMemoryUserDetailsManager 来存储在内存中;
  • 我们默认生成了两个用户,一个是“waylau” 是拥有“USER”角色权限;另一个是“admin” 是拥有“USER”以及“ADMIN”角色权限

运行

运行程序,访问http://localhost:8080 页面。
当我们试图访问以下页面时,提示需要用户登录:

  • “用户管理”:对应 “/users” URL;
  • “管理员管理”:对应 “/admins” URL

登录页面:

Hello World - 图2

使用具有“USER”角色授权的“waylau”用户登录,可以访问“用户管理”页面:

Hello World - 图3

“waylau”用户登录可以访问“管理员管理”页面时,提示拒绝访问:

Hello World - 图4

此时,我们换用 “admin”账号登录即可访问该页面。返回首页,我们能看到该用户的权限信息:

Hello World - 图5

自定义 403 页面

默认的提示拒绝访问页面太丑,我需要自定义一个页面。我们在返回的页面里面提示“拒绝访问!”。

同时,我要在配置类里面,配置重定向的内容:

  1. ......
  2. .formLogin() //基于 Form 表单登录验证
  3. .loginPage("/login").failureUrl("/login-error") // 自定义登录界面
  4. .and()
  5. .exceptionHandling().accessDeniedPage("/403"); // 处理异常,拒绝访问就重定向到 403 页面
  6. ......

最终效果:

Hello World - 图6

处理登出

使用WebSecurityConfigurerAdapter时,应用会自动提供注销功能。 默认情况下,访问 URL /logout来注销用户。注销动作,做了以下几个事情:

  • 使 HTTP 会话无效
  • 清除配置了 RememberMe 身份认证的信息
  • 清除 SecurityContextHolder
  • 重定向到/login?logout

与配置登录功能类似,我们也可以使用各种选项进一步自定义登出要求,比如:

  1. http.logout().logoutSuccessUrl("/"); // 成功登出后,重定向到 首页

这样,成功登出后,重定向到 首页。

其他的还可以选项还有很多:

  1. protected void configure(HttpSecurity http) throws Exception {
  2. http
  3. .logout()
  4. .logoutUrl("/my/logout") // 1
  5. .logoutSuccessUrl("/my/index")
  6. .logoutSuccessHandler(logoutSuccessHandler) // 2
  7. .invalidateHttpSession(true) // 3
  8. .addLogoutHandler(logoutHandler) // 4
  9. .deleteCookies(cookieNamesToClear) // 5
  10. .and()
  11. ...
  12. }
  • (1)自定义触发登出的 URL
  • (2)指定一个自定义LogoutSuccessHandler
  • (3)指定在注销时是否使 HttpSession 无效。默认情况下是 true
  • (4)添加 LogoutHandler。默认情况下,SecurityContextLogoutHandler 作为最后一个 LogoutHandler 被添加
  • (5)允许指定在注销成功时要删除的 Cookie 的名称。这是一个显式添加 CookieClearingLogoutHandler 的快捷方式