集成SpringSecurity
# 12.2 集成 Spring Security
在 Spring Boot 中集成 Spring Security 一如既往地使用了启动器依赖,在创建 Spring Starter 项目时选中“Spring Security”依赖即可。
# 12.2.1 基本集成
添加“Spring Security”启动器依赖后,即完成了 Spring Security,项目的 pom 文件中的依赖为:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2
3
4
在项目中添加一个控制器 HelloController 类,测试 Spring Security 是否正确工作。
package com.example.security.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/security/")
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "Hello, Security.";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
运行应用后,打开浏览器,访问http://localhost:8080/security/hello (opens new window),弹出Spring Security 提供的默认认证页面,用户名为 user,密码是在启动应用时生成的一个随机字符串(STS控制台中的密码信息:Using generated security password: 0585d7e0-dde1-4b9e-8352-27eb72517803)。
默认密码在应用启动时,打印在控制台中,当然每次启动时生成的密码都不一样。
使用 user 用户名和生成的密码(这里是“0585d7e0-dde1-4b9e-8352-27eb72517803”)登录后,可正确访问 HelloController 返回的信息。
通过查看 Spring Security 的源码org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
类,可以看到 inMemoryUserDetailsManager 方法中使用的SecurityProperties.User user
属性提供了用户名和密码。由 getOrDeducePassword 方法通过日志logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
将生成的默认密码打印到控制台。
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(
User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles)).build());
}
2
3
4
5
6
7
8
进一步查看这个 User 类,可以看到默认登录用户为 user,密码为一个 uuid 字符串。
public static class User {
/**
* Default user name.
*/
private String name = "user";
/**
* Password for the default user name.
*/
private String password = UUID.randomUUID().toString();
/**
* Granted roles for the default user name.
*/
private List<String> roles = new ArrayList<>();
private boolean passwordGenerated = true;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
if (!StringUtils.hasLength(password)) {
return;
}
this.passwordGenerated = false;
this.password = password;
}
public List<String> getRoles() {
return this.roles;
}
public void setRoles(List<String> roles) {
this.roles = new ArrayList<>(roles);
}
public boolean isPasswordGenerated() {
return this.passwordGenerated;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
如果我们简单的在 application.properties 文件中指定用户名和密码:
spring.security.user.name=kevin
spring.security.user.password=6998
spring.security.user.roles=admin
2
3
则应用在启动时,就不会生成随机的 uuid 密码,也不会在控制台中打印Using generated security password: 0585d7e0-dde1-4b9e-8352-27eb72517803
这样的信息了。
这个时候,通过浏览器访问http://localhost:8080/security/hello (opens new window) 则需要输入 kevin 用户和 6998 密码,就可以正常访问后台业务逻辑并返回正确的结果,并且该用户具有 admin 角色(虽然这个示例中角色没有任何用处)。
# 12.2.2 密码加密
除了在学习环境下,为了安全起见,密码存储都必须要加密。
Spring Security 通过实现 PasswordEncoder 接口,提供了众多的密码加密类(包括供我们在学习环境下使用的 NoOpPasswordEncoder 明文密码编码器),供我们在实际工作中根据需要选用。
奇葩的 NoOpPasswordEncoder 类:encode(CharSequence rawPassword) 方法的返回值为encode(CharSequence rawPassword) ,也就是密码编码返回明文,所以千万不要在生产环境中使用。
修改 SecurityConfiguration 类,添加 BCryptPasswordEncoder 密码编码器。
BCryptPasswordEncoder,使用了 BCrypt 强哈希方法来加密密码。
package com.example.security.config;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
PasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password(new BCryptPasswordEncoder().encode("admin")).roles("ADMIN", "USER")
.and()
.withUser("kevin").password(new BCryptPasswordEncoder().encode("6998")).roles("USER")
.and()
.withUser("roy").password(new BCryptPasswordEncoder().encode("3525")).roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.antMatchers("/user/**")
.hasRole("USER")
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
上面的示例中 PasswordEncoder 使用了 BCryptPasswordEncoder 实现类,并且在基于内存的认证器中给出用户密码时,使用了 BCryptPasswordEncoder 加密密码(password(new BCryptPasswordEncoder().encode("admin")))。这样,内存中存储的密码时通过 BCrypt 加密的密码,用户在登录页面输入密码(例如 admin 用户的密码 admin)后,Spring Security 使用 PasswordEncoder 对密码进行编码后与内存中存储的加密后的密码对比,完成认证,正确后就放行。
# 12.2.3 基于内存的认证
在本小节的学习过程中,通过将用户、密码和角色配置在内存中,会更方便集中精力学习 Spring Security。
注释掉上一小节 application.properties 文件中指定的用户名、密码和角色。
后续小节我们会通过数据库来完成一个RBAC的安全控制示例。
package com.example.security.config;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("admin").roles("ADMIN", "USER")
.and()
.withUser("kevin").password("6998").roles("USER")
.and()
.withUser("roy").password("3525").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.antMatchers("/user/**")
.hasRole("USER")
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
上述代码,首先注册一个 PasswordEncoder(密码加密编码器类),这里为了测试简便,使用了不加密密码。在实际生产环境下绝对不能使用这个密码编码器类。
然后使用 AuthenticationManagerBuilder.inMemoryAuthentication 的内存认证,通过编码添加了 3 个用户和对应的角色("ADMIN", "USER")。
最后使用 HttpSecurity 为不同的资源匹配了不同的角色访问权限。
添加 AdminController 控制器类,提供“/admin/hello”资源。
package com.example.security.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/admin/")
public class AdminController {
@RequestMapping("/hello")
public String hello() {
return "admin.hello";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
添加 UserController 控制器类,提供“/user/hello”资源。
package com.example.security.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user/")
public class UserController {
@RequestMapping("/hello")
public String hello() {
return "user.hello";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
启动应用,测试:
使用 admin 用户登录,可以正确访问“/admin/hello”和“/user/hello”资源。
使用 kevin 用户登录,可以正确访问“/user/hello”资源,不能正确访问“/admin/hello”资源,错误信息如下:
以上就是一个基于内存认证的最简单的 RBAC(基于角色的存取控制)权限控制系统:
- kevin 拥有 USER 角色;
- “/user/**”资源授权给 USER 角色;
- kevin 登录后拥有 USER 角色,可以访问“/user/hello”资源。
# 12.2.4 登录表单/注销配置
虽然 Spring Security 提供了默认的登录表单,但是在实际工作中绝大多数情况都用不上。基本上在所有的项目/产品中,都会使用自定义的登录表单。当然,注销登录(或者叫做登出),也会使用自定义的配置。
通过查看 Spring Security 源码,可以看到默认登录表单在org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.generateLoginPageHtml
方法中构建。
在 DefaultLoginPageGeneratingFilter 类的 doFilter 方法中response.getWriter().write(loginPageHtml)
代码向浏览器输出登录表单。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
boolean loginError = isErrorPage(request);
boolean logoutSuccess = isLogoutSuccess(request);
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
String loginPageHtml = generateLoginPageHtml(request, loginError,
logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
return;
}
chain.doFilter(request, response);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
本小节,我们使用 Thymeleaf 模板+基于内存认证方式来学习如何自定义登录表单。
首先,我们添加几个简单的页面,然后通过 Spring Security 基于内存的配置设置其用户、角色、自定义登录表单、注销等操作。
# 12.2.4.1 首页
创建src/main/resources/templates/index.html
页面,作为项目的首页。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>示例首页</title>
</head>
<body>
<h1>示例首页</h1>
<p>登录成功后,自动跳转到这里</p>
<p>
点击 <a th:href="@{/user/hello}">这里</a> 进入User Hello页面。
</p>
<p>
点击 <a th:href="@{/admin/hello}">这里</a> 进入Admin Hello页面。
</p>
<form th:action="@{/logout}" method="post">
<input type="submit" value="退出(登出,logout)" />
</form>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 12.2.4.2 登录页面
创建src/main/resources/templates/login.html
页面,作为项目的登录页面。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>自定义登录示例</title>
</head>
<body>
<h1>请你登录</h1>
<div th:if="${param.error}">用户名或密码错</div>
<div th:if="${param.logout}">您已注销成功</div>
<form th:action="@{/login}" method="post">
<div>
<label> 用户名 : <input type="text" name="login_username" />
</label>
</div>
<div>
<label> 密码 : <input type="password" name="login_password" />
</label>
</div>
<div>
<input type="submit" value="登录" />
</div>
</form>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在登录页面中,特意将用户名 input 设为 login_username,演示匹配后续的自定义登录页面配置信息。
# 12.2.4.3 管理页面
创建src/main/resources/templates/admin/hello.html
,作为项目的管理页面,按照本示例设置,需要 ADMIN 角色的用户才可以访问。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Admin.Hello Page</title>
</head>
<body>
<h1>Admin Page</h1>
<p th:text="${user}"/>
<p><a th:href="@{/index}">返回</a></p>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
# 12.2.4.4 用户页面
创建src/main/resources/templates/user/hello.html
,作为项目的用户页面,按照本示例设置,需要 USER 角色的用户才可以访问。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>User.Hello Page</title>
</head>
<body>
<h1>User Page</h1>
<p><p th:text="${user}"/></p>
<p><a th:href="@{/index}">返回</a></p>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
# 12.2.4.5 其他页面
创建src/main/resources/templates/other/hello.html
,作为项目的其他页面,按照本示例设置,任何登录用户(任何角色)都可以访问。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Other.Hello Page</title>
</head>
<body>
<h1>Other Page</h1>
<p><p th:text="${user}"/></p>
<p><a th:href="@{/index}">返回</a></p>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
# 12.2.4.6 页面 Controller
新建一个控制页面跳转的 PageController,将上述4个页面串接起来。
package com.example.security.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class PagesController {
@RequestMapping("/index")
public String index() {
return "/index";
}
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login() {
return "/login";
}
@RequestMapping("/admin/hello")
public ModelAndView adminHello() {
ModelAndView mv = new ModelAndView();
mv.addObject("user", "admin");
mv.setViewName("/admin/hello");
return mv;
}
@RequestMapping("/user/hello")
public ModelAndView userHello() {
ModelAndView mv = new ModelAndView();
mv.addObject("user", "kevin");
mv.setViewName("/user/hello");
return mv;
}
@RequestMapping("/other/hello")
public ModelAndView otherHello() {
ModelAndView mv = new ModelAndView();
mv.addObject("user", "other");
mv.setViewName("/other/hello");
return mv;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 12.2.4.7 Spring Security 配置
在启用了 Spring Security 的 Spring Boot 应用中,我们可以通过自定义表单 html 页面,然后通过 HttpSecurity 的 formLogin 和 logout 自定义登录页面和注销(登出)操作和行为。
package com.example.security.config;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password(new BCryptPasswordEncoder().encode("admin")).roles("ADMIN", "USER")
.and()
.withUser("kevin").password(new BCryptPasswordEncoder().encode("6998")).roles("USER")
.and()
.withUser("roy").password(new BCryptPasswordEncoder().encode("3525")).roles("OTHER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login")
.usernameParameter("login_username")
.passwordParameter("login_password")
.failureUrl("/login?error")
.defaultSuccessUrl("/index")
.permitAll()
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.permitAll()
.and().authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.anyRequest()
.authenticated();
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/config/**", "/css/**", "/./images/**", "/js/**");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
formLogin().permitAll()
代码指示 Spring Security 允许任何用户访问 formLogin 与之关联的 URL(/login);logout().permitAll()
代码指示 Spring Security 允许任何用户访问 logout 与之关联的 URL(/logout);authorizeRequests().anyRequest().authenticated()
代码指示 Spring Security 允许登录(通过认证的)用户访问所有的页面(比如“other/hello.html”页面);antMatchers("/admin/**").hasRole("ADMIN")
具有 ADMIN 角色的登录用户才都访问 /admin/ 开头的页面。
# 12.2.4.8 测试
启动应用程序,打开浏览器,输出http://localhost:8080/ (opens new window) 地址,自动跳转到登录页面。
输入错误的用户名,会跳转回登录页面,并且显示错误信息:用户名或密码错。
输入正确的用户名和密码,登录后会跳转到首页。
点击 User Hello 页面的链接,可以正确访问 User Hello 页面。
返回首页,后点击 Admin Hello 页面的链接,由于 Kevin 用户不具有 ADMIN 角色,所以不可以访问 Admin Hello 页面。
使用 roy 用户登录(其用户角色为 OTHER),既不能访问 admin/hello 页面,也不能访问 user/hello 页面,只能访问 other/hello 页面。当然,admin 和 kevin 用户登录,都可以访问 other/hello 页面。
在首页,点击退出(登出,logout)
按钮,可以正确注销当前登录。
以上测试,符合项目权限控制预期。
本小节示例项目代码:
https://github.com/gyzhang/SpringBootCourseCode/tree/master/spring-boot-security (opens new window)
# 12.2.5 基于数据库的认证
在我们实际的工作中,几乎所有的认证授权,都是基于数据库中存储的用户授权数据来的完成的。
在Spring Security中,通过org.springframework.security.core.userdetails.UserDetails
l和org.springframework.security.core.userdetails.UserDetailsService
两个接口完成程序定义(在这两个接口的实现类中访问数据库中的数据)的用户服务,然后通过AuthenticationManagerBuilder.userDetailsService(customUserService())
将我们通过数据库存储的用户角色(授权)连接到 Spring Security,完成用户认证与授权(Authentication 和 Authorization)。
@Bean
UserDetailsService customUserService() { // 注册UserDetailsService 的bean
return new CustomUserDetailsService();
}
@Bean
PasswordEncoder passwordEncoder() {
PasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder;
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService()).passwordEncoder(passwordEncoder());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
本小节,我们使用 MySQL 数据库,通过 MyBatis 来学习如何基于数据库来支持 Spring Security 的用户认证。
创建项目,添加数据库及 Spring Security 相关的启动器依赖。
项目的 pom 依赖为:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
项目的 application.yml 文件中配置数据库连接及 MyBatis 属性。
# mysql数据源配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/ssdemo?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# mybatis 配置信息
mybatis:
mapper-locations: classpath:mapping/*Mapper.xml
type-aliases-package: com.example.security.db.entity
configuration:
map-underscore-to-camel-case: true
2
3
4
5
6
7
8
9
10
11
12
13
mybatis.configuration.map-underscore-to-camel-case=true
是打开字段下划线和 java 实体类中属性“驼峰命名”之间的映射。例如 role_code 字段自动映射为 roleCode,反之亦然。
# 12.2.5.1 创建数据库表
基于最简学习案例原则,设计的数据库表(用户、角色和用户角色关联表)只包含最少的字段。
drop table if exists sys_role;
drop table if exists sys_user;
drop table if exists sys_user_role;
/*==============================================================*/
/* Table: sys_role */
/*==============================================================*/
create table sys_role
(
role_code varchar(64) not null,
role_name varchar(64),
primary key (role_code)
);
/*==============================================================*/
/* Table: sys_user */
/*==============================================================*/
create table sys_user
(
username varchar(64) not null,
password varchar(64),
primary key (username)
);
/*==============================================================*/
/* Table: sys_user_role */
/*==============================================================*/
create table sys_user_role
(
id varchar(64) not null,
username varchar(64),
role_code varchar(64),
primary key (id)
);
alter table sys_user_role add constraint FK_Reference_1 foreign key (username)
references sys_user (username) on delete restrict on update restrict;
alter table sys_user_role add constraint FK_Reference_2 foreign key (role_code)
references sys_role (role_code) on delete restrict on update restrict;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
在 3 张表中插入一些测试数据。
INSERT INTO `sys_user` VALUES ('admin', 'admin');
INSERT INTO `sys_user` VALUES ('kevin', '6998');
INSERT INTO `sys_user` VALUES ('roy', '3525');
INSERT INTO `sys_role` VALUES ('ROLE_ADMIN', '管理角色');
INSERT INTO `sys_role` VALUES ('ROLE_OTHER', '其他角色');
INSERT INTO `sys_role` VALUES ('ROLE_USER', '用户角色');
INSERT INTO `sys_user_role` VALUES ('1', 'admin', 'ROLE_ADMIN');
INSERT INTO `sys_user_role` VALUES ('2', 'admin', 'ROLE_USER');
INSERT INTO `sys_user_role` VALUES ('3', 'kevin', 'ROLE_USER');
INSERT INTO `sys_user_role` VALUES ('4', 'roy', 'ROLE_OTHER');
2
3
4
5
6
7
8
9
10
11
12
# 12.2.5.2 创建实体类
根据数据库设计,创建 3 个实体类,供后续 MyBatis 输出据库操作用。
创建 SysUser 实体类(对应 sys_user 表)。
package com.example.security.db.entity;
public class SysUser {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
创建 SysRole 实体类(对应 sys_role 表)。
package com.example.security.db.entity;
public class SysRole {
private String roleCode;
private String roleName;
public String getRoleCode() {
return roleCode;
}
public void setRoleCode(String roleCode) {
this.roleCode = roleCode;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
创建 SysUserRole 实体类(对应 sys_user_role 表)。
package com.example.security.db.entity;
public class SysUserRole {
private String id;
private String username;
private String roleCode;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getRoleCode() {
return roleCode;
}
public void setRoleCode(String roleCode) {
this.roleCode = roleCode;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 12.2.5.3 创建 DAO
最简起见,我们创建 SysUser 和 SysUserRole 对应的 DAO(mapper)。
SysUser 的 mapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.security.db.mapper.SysUserMapper">
<select id="selectSysUser" resultType="SysUser">
select * from sys_user where username = #{username}
</select>
</mapper>
2
3
4
5
6
7
8
9
SysUser 的 Mapper:
package com.example.security.db.mapper;
import org.springframework.stereotype.Repository;
import com.example.security.db.entity.SysUser;
@Repository
public interface SysUserMapper {
SysUser selectSysUser(String username);
}
2
3
4
5
6
7
8
9
10
11
12
SysUserRole 的 mapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.security.db.mapper.SysUserRoleMapper">
<select id="selectSysUserRole" resultType="SysUserRole">
select * from sys_user_role where username = #{username}
</select>
</mapper>
2
3
4
5
6
7
8
9
SysUserRole 的 Mapper:
package com.example.security.db.mapper;
import java.util.List;
import org.springframework.stereotype.Repository;
import com.example.security.db.entity.SysUserRole;
@Repository
public interface SysUserRoleMapper {
List<SysUserRole> selectSysUserRole(String username);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
以上两个 mapper,完成用户的用户角色关系这两张表的访问。
# 12.2.5.4 创建 CustomUserService
CustomUserService 实现org.springframework.security.core.userdetails.UserDetailsService
接口,完成用户及用户所拥有的角色信息从数据库中获取的功能。
创建 CustomUserService 类:
package com.example.security.db.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.example.security.db.entity.SysUser;
import com.example.security.db.entity.SysUserRole;
import com.example.security.db.mapper.SysUserMapper;
import com.example.security.db.mapper.SysUserRoleMapper;
public class CustomUserService implements UserDetailsService {
@Autowired
SysUserMapper sysUserMapper;
@Autowired
SysUserRoleMapper sysUserRoleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserMapper.selectSysUser(username);
String password = sysUser.getPassword();
List<SysUserRole> sysUserRoles = sysUserRoleMapper.selectSysUserRole(username);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (SysUserRole sysUserRole : sysUserRoles) {
authorities.add(new SimpleGrantedAuthority(sysUserRole.getRoleCode()));
}
return new User(username, password, authorities);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 12.2.5.5 创建 Controller
按照上一小节创建页面访问的控制器 PagesController:
package com.example.security.db.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class PagesController {
@RequestMapping("/index")
public String index() {
return "/index";
}
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login() {
return "/login";
}
@RequestMapping("/admin/hello")
public ModelAndView adminHello() {
ModelAndView mv = new ModelAndView();
mv.addObject("user", "admin");
mv.setViewName("/admin/hello");
return mv;
}
@RequestMapping("/user/hello")
public ModelAndView userHello() {
ModelAndView mv = new ModelAndView();
mv.addObject("user", "kevin");
mv.setViewName("/user/hello");
return mv;
}
@RequestMapping("/other/hello")
public ModelAndView otherHello() {
ModelAndView mv = new ModelAndView();
mv.addObject("user", "other");
mv.setViewName("/other/hello");
return mv;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 12.2.5.6 Thymeleaf 页面
复用上一小节(12.2.4)中的 Thymeleaf 页面。
# 12.2.5.7 Spring Security 配置
创建配置类 SecurityConfiguration,其中关键的一句代码(将数据库访问用户信息的 CustomUserService 类注入)是auth.userDetailsService(customUserService());
package com.example.security.db.config;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.example.security.db.service.CustomUserService;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
UserDetailsService customUserService(){
return new CustomUserService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login")
.usernameParameter("login_username")
.passwordParameter("login_password")
.failureUrl("/login?error")
.defaultSuccessUrl("/index")
.permitAll()
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.permitAll()
.and().authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.anyRequest()
.authenticated();
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/config/**", "/css/**", "/./images/**", "/js/**");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
在学习过程中为了方便往数据库中录入用户密码信息(明文),所以上面的配置中使用了 NoOpPasswordEncoder 这个奇葩的密码编码器,一如既往地强调:严禁在非学习系统中使用这个(明文的)密码编码器。
Spring Security 配置中使用角色默认前缀为“ROLE_”,例如使用角色“ADMIN”,在数据库表中应该存储为“ROLE_ADMIN”。参考
org.springframework.security.access.vote.RoleVoter
类中的rolePrefix
属性。
# 12.2.5.8 启动类
启动类中别忘了 MyBatis 的 @MapperScan 注解。
package com.example.security.db;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.security.db.mapper")
public class SpringBootSecurityDbApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootSecurityDbApplication.class, args);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 12.2.5.9 测试
启动项目测试,和上一小节(12.2.4)效果一致。
本小节示例项目代码: