书成

再这样堕落下去就给我去死啊你这混蛋!!!

0%

SpringSecurity使用

Spring Security

在 Java Web 项目中,一般使用 Servlet 过滤器对请求进行拦截,然后再 Filter 中通过自己的验证逻辑来决定是否放行请求。同样的,在 Spring Security 中,也是基于这个原理,在进入到 DispatcherServlet 之前就对请求进行拦截,然后通过一定的验证,决定是否放行请求到系统。

引入 Spring Security

在项目中添加如下依赖即可引入 Spring Security

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Spring Security 被引入后,默认就会保护项目中所有的接口,启动项目时会看到如下日志:

之后在想要访问系统的时候,会弹出如下的界面,默认用户名是 user,密码在启动日志中每次启动随机生成:

你也可以在配置文件中配置自己的用户名和密码:

1
2
spring.security.user.name=user
spring.security.user.password=123456

配置 Spring Security

Spring Security 提供了 SecurityConfigurer 接口实现对 Spring Security 的配置,对 Web 工程提供了专门的 WebSecurityConfigurer 接口,并且在这个接口的基础上提供了一个抽象类 WebSecurityConfigurerAdapter,只需要继承这个类并覆盖其中的三个 configure 方法即可配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置用户签名服务,主要使用了 user-details 机制,还可以赋予用户不同角色
// auth 参数是签名服务构造器,构建用户具体权限控制
super.configure(auth);
}

@Override
public void configure(WebSecurity web) throws Exception {
// 配置 Filter 链
super.configure(web);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置拦截保护的请求
super.configure(http);
}
}

使用内存签名

内存签名就是将用户信息放在内存中,配置如下:

1
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
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); // 密码编码器
auth.inMemoryAuthentication()
.passwordEncoder(passwordEncoder)
.withUser("admin")
.password("$2a$10$0i6jY5Mem3JLlX2CTs9VnebYgHVcsCmm3k9nFTvQ48RKWm7li4tNa") // 密码123
.roles("ADMIN")
.and()
.withUser("user")
.password("$2a$10$0i6jY5Mem3JLlX2CTs9VnebYgHVcsCmm3k9nFTvQ48RKWm7li4tNa")
.roles("USER");
}

@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 开启http拦截
.antMatchers("/admin/**").hasRole("ADMIN") // 配置角色与url的规则
.antMatchers("/user/**").hasRole("USER")
.anyRequest().authenticated() // 其余url只要登录就能访问
.and()
.formLogin()
.and()
.csrf().disable(); // 取消跨域保护
}
}

在控制器中添加对应方法:

1
2
3
4
5
6
7
8
9
@GetMapping("/admin/hello")
public String adminHello(){
return "ADMIN Hello";
}

@GetMapping("/user/hello")
public String userHello(){
return "USER Hello";
}

然后启动浏览器用不同的用户登录访问不同 url 查看效果即可。

登录的更多配置

1
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
    protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 开启http拦截
.antMatchers("/admin/**").hasRole("ADMIN") // 配置角色与url的规则
.antMatchers("/user/**").hasRole("USER")
.anyRequest().authenticated() // 其余url只要登录就能访问
.and()
.formLogin()
.loginProcessingUrl("/doLogin") // 登录接口
// .loginPage("/login") // 配置自己的登录页面,前后端分离一般不需要
// .usernameParameter("uname") // 配置参数名
// .passwordParameter("passwd") // 配置参数名
// .successForwardUrl("/home") // 配置登录成功跳转页面
.successHandler(new AuthenticationSuccessHandler() { // 登录成功处理器,前后端分离常用
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth)
throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("msg", "登录成功");
map.put("user", auth.getPrincipal()); // 获得登录的用户信息 user-details 机制
writer.write(new ObjectMapper().writeValueAsString(map));
writer.flush();
writer.close();
}
})
.failureHandler(new AuthenticationFailureHandler() { // 登录失败处理器
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e)
throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("msg", "登录失败");
writer.write(new ObjectMapper().writeValueAsString(map));
writer.flush();
writer.close();
}
})
.permitAll() // 登录接口允许所有访问
.and()
.logout() // 配置注销
.logoutUrl("/logout") // 配置注销接口
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth)
throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("msg", "注销成功");
writer.write(new ObjectMapper().writeValueAsString(map));
writer.flush();
writer.close();
}
})
.and()
.csrf().disable(); // 取消跨域保护
}

使用数据库定义用户

大部分情况下用户信息会放在数据库,首先准备好几张表:

1
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
create table role
(
id int auto_increment
primary key,
name varchar(20) null,
nameZh varchar(30) null
);
create table user
(
id int auto_increment
primary key,
username varchar(30) null,
password varchar(100) null,
enabled tinyint(1) default 1 null,
locked tinyint(1) default 0 null
);
create table user_role
(
id int auto_increment
primary key,
uid int null,
rid int null,
constraint user_role_role_id_fk
foreign key (rid) references role (id),
constraint user_role_user_id_fk
foreign key (uid) references user (id)
);

在数据库中加入对应测试数据:

添加 User 类以及 Role 类如下:

1
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// 实现 UserDetails 接口并重写其中的方法
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean locked;
// 用户所拥有的所有角色
private List<Role> roles;

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return !this.locked;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return this.enabled;
}

public void setUsername(String username) {
this.username = username;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
// 必须以 ROLE_ 开头
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
}
return authorities;
}

@Override
public String getUsername() {
return username;
}

@Override
public String getPassword() {
return password;
}

public List<Role> getRoles() {
return roles;
}

public void setRoles(List<Role> roles) {
this.roles = roles;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public void setPassword(String password) {
this.password = password;
}

public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}

public void setLocked(Boolean locked) {
this.locked = locked;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", enabled=" + enabled +
", locked=" + locked +
'}';
}
}
1
2
3
4
5
public class Role {
private Integer id;
private String name;
private String nameZh;
}

实现 UserService 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
// 实现 UserDetailsService 接口
public class UserService implements UserDetailsService {

@Autowired
UserMapper userMapper;

@Override
// 参数是 username ,返回一个 UserDetails 实例
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if(user == null){
throw new UsernameNotFoundException("用户不存在!!");
}
user.setRoles(userMapper.getUserRolesById(user.getId()));
return user;
}
}

实现 UserMapper 接口与 UserMapper.xml 如下:

1
2
3
4
5
6
@Repository
public interface UserMapper {
public List<Role> getUserRolesById(Integer id);

public User loadUserByUsername(String username);
}
1
2
3
4
5
6
7
8
9
10
11
<?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="top.liuzhenhui.securitydemo.mapper.UserMapper">

<select id="loadUserByUsername" resultType="top.liuzhenhui.securitydemo.model.User">
select * from security.user where username = #{username}
</select>
<select id="getUserRolesById" resultType="top.liuzhenhui.securitydemo.model.Role">
select r.* from role r, user_role ur where ur.uid = #{id} and r.id = ur.rid;
</select>
</mapper>

在 SecurityConfig 中使用数据库认证:

1
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
UserService userService;

@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用数据库认证
auth.userDetailsService(userService);
}

@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 开启http拦截
.antMatchers("/admin/**").hasRole("ADMIN") // 配置角色与url的规则
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/dba/**").hasRole("DBA")
.anyRequest().authenticated() // 其余url只要登录就能访问
.and()
.formLogin()
.loginProcessingUrl("/doLogin") // 登录接口
.successHandler(new AuthenticationSuccessHandler() { // 登录成功处理器,前后端分离常用
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth)
throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("msg", "登录成功");
map.put("user", auth.getPrincipal()); // 获得登录的用户信息 user-details 机制
writer.write(new ObjectMapper().writeValueAsString(map));
writer.flush();
writer.close();
}
})
.failureHandler(new AuthenticationFailureHandler() { // 登录失败处理器
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e)
throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("msg", "登录失败");
map.put("exception", e.getMessage());
writer.write(new ObjectMapper().writeValueAsString(map));
writer.flush();
writer.close();
}
})
.permitAll() // 登录接口允许所有访问
.and()
.logout() // 配置注销
.logoutUrl("/logout") // 配置注销接口
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth)
throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("msg", "注销成功");
writer.write(new ObjectMapper().writeValueAsString(map));
writer.flush();
writer.close();
}
})
.and()
.csrf().disable(); // 取消跨域保护
}
}

之后在控制器中添加对应测试 url,启动项目测试即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/admin/hello")
public String adminHello(){
return "ADMIN Hello";
}

@GetMapping("/user/hello")
public String userHello(){
return "USER Hello";
}

@GetMapping("/dba/hello")
public String dbaHello(){
return "DBA Hello";
}

角色继承

在有的情况下,不同角色之间有继承关系,可以在 SecurityConfig 中添加如下代码加入角色继承关系,之后重启项目测试:

1
2
3
4
5
6
7
@Bean
RoleHierarchy roleHierarchy(){
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_DBA > ROLE_ADMIN \n ROLE_ADMIN > ROLE_USER";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}

动态权限配置

在之前的代码中,一个角色能登录哪些 url 是写死在代码中的,正常情况下一般要存在数据库中,这样的话方便修改。

在数据库中添加新的表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
create table menu
(
id int auto_increment
primary key,
pattern varchar(20) null
);

create table menu_role
(
id int auto_increment
primary key,
mid int null,
rid int null,
constraint menu_role_menu_id_fk
foreign key (mid) references menu (id),
constraint menu_role_role_id_fk
foreign key (rid) references role (id)
);

添加对应测试数据:

然后自定义一个实现 FilterInvocationSecurityMetadataSource 接口的类如下:

1
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
@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource {

AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
MenuService menuService;

@Override
// 根据请求地址获得需要的角色
public Collection<ConfigAttribute> getAttributes(Object o)
throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) o).getRequestUrl(); // 获得请求地址
List<Menu> menus = menuService.getAllMenus();
for (Menu menu : menus) {
if (antPathMatcher.match(menu.getPattern(), requestUrl)) { // 匹配上了就查找角色
List<Role> roles = menu.getRoles();
String[] rolesStr = new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
rolesStr[i] = roles.get(i).getName();
}
return SecurityConfig.createList(rolesStr); // 传入数组
}
}
return SecurityConfig.createList("ROLE_LOGIN"); // 默认的角色是登录,相当于特殊标记符
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

@Override
public boolean supports(Class<?> aClass) {
return true;
}
}

自定义一个实现了 AccessDecisionManager 接口的类如下:

1
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
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection)
throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute attribute : collection) {
if("ROLE_LOGIN".equals(attribute.getAttribute())){
// 如果是未登录的匿名用户就抛异常
if(authentication instanceof AnonymousAuthenticationToken){
throw new AccessDeniedException("用户未登录");
}else{
return;
}
}
// 获得当前用户拥有的角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
// 如果已经拥有角色就放行请求
if(authority.getAuthority().equals(attribute.getAttribute())){
return;
}
}
}
throw new AccessDeniedException("非法请求");
}

@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}

@Override
public boolean supports(Class<?> aClass) {
return true;
}
}

修改 SecurityConfig 如下:

1
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
UserService userService;
@Autowired
MyFilter myFilter;
@Autowired
MyAccessDecisionManager myAccessDecisionManager;

@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

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

@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 开启http拦截
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
// 将自定义的类引入
o.setAccessDecisionManager(myAccessDecisionManager);
o.setSecurityMetadataSource(myFilter);
return o;
}
})
.and()
.formLogin()
.loginProcessingUrl("/doLogin") // 登录接口
.successHandler(new AuthenticationSuccessHandler() { // 登录成功处理器,前后端分离常用
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth)
throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("msg", "登录成功");
map.put("user", auth.getPrincipal()); // 获得登录的用户信息 user-details 机制
writer.write(new ObjectMapper().writeValueAsString(map));
writer.flush();
writer.close();
}
})
.failureHandler(new AuthenticationFailureHandler() { // 登录失败处理器
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e)
throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("msg", "登录失败");
map.put("exception", e.getMessage());
writer.write(new ObjectMapper().writeValueAsString(map));
writer.flush();
writer.close();
}
})
.permitAll() // 登录接口允许所有访问
.and()
.logout() // 配置注销
.logoutUrl("/logout") // 配置注销接口
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth)
throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("msg", "注销成功");
writer.write(new ObjectMapper().writeValueAsString(map));
writer.flush();
writer.close();
}
})
.and()
.csrf().disable(); // 取消跨域保护
}
}

之后启动项目测试不同用户登录能访问的 url 即可。