SpringSecurity

SpringSecurity是一个针对于Spring项目的安全框架,侧重于为Spring项目提供身份验证和授权。

将我们之前使用拦截器,过滤器进行权限检查,和身份验证的操作进行了简化,并提供了一套 Web 应用安全性的完整解决方案。

SpringSecurity使用

仅需引入spring-boot-starter-security模块,进行少量的配置,便可以实现强大的安全管理。

  • WebSecurityConfigureAdapter:自定义Security策略
  • AuthenticationManagerBuilder:自定义认证策略

SpringScurity的两个主要目标就是认证授权

1、引入SpringSecurity模块

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

2、创建config目录,添加WebSecurityConfig类

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置用户信息 模拟内存用户数据
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("root")
.password(passwordEncoder().encode("root"))
.roles("ADMIN", "USER")
.authorities("sys:add", "sys:delete", "sys:query", "sys:update")
.and()
.withUser("zyz")
.password(passwordEncoder().encode("zyz"))
.roles("USER")
.authorities("sys:query");
}

/**
* 密码加密器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

/**
* http控制
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {

// 关闭跨站请求伪造
http.csrf().disable();

// 请求拒绝的处理器
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());

// 登录成功和失败的跳转 前后端返回json字符串
http.formLogin()
.loginProcessingUrl("/loginNew")// 自定义登录的请求地址
.usernameParameter("userName")// 取别名
.passwordParameter("password")
// .successForwardUrl("/welcome")
// .failureForwardUrl("/login");
.successHandler(successHandler())
.failureHandler(failureHandler())
.permitAll();// 放行接口

// 自定义登出的请求地址
http.logout().logoutUrl("/logoutNew");

// 访问权限的控制 在controller方法上使用@PreAuthorize注解代替(启动类需要开启@EnableGlobalMethodSecurity(prePostEnabled = true))
// http.authorizeRequests()
// .antMatchers("/addUser").hasAuthority("sys:add")
// .antMatchers("/delUser").hasAnyAuthority("sys:del")
// .antMatchers("/updateUser").hasAnyAuthority("sys:update")
// .antMatchers("/queryUser").hasAnyAuthority("sys:query")
// .antMatchers("/testUser").hasAnyAuthority("sys:test");

// 其他接口登录后才能访问
http.authorizeRequests()
.anyRequest()
.authenticated();
}


/**
* 登录成功后的处理 将用户信息存入redis 并返回前端一个token
* @return
*/
@Bean
public AuthenticationSuccessHandler successHandler() {
return (request, response, authentication) -> {
// 组装数据给前端
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.OK.value());
result.put("msg", "登录成功");
// 设置编码
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
};
}

/**
* 登录失败后的处理
* @return
*/
@Bean
public AuthenticationFailureHandler failureHandler() {
return (request, response, exception) -> {
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.UNAUTHORIZED.value());
if (exception instanceof LockedException) {
result.put("msg", "账户被锁定,登陆失败!");
} else if (exception instanceof BadCredentialsException) {
result.put("msg", "账户或者密码错误,登陆失败!");
} else if (exception instanceof DisabledException) {
result.put("msg", "账户被禁用,登陆失败!");
} else if (exception instanceof AccountExpiredException) {
result.put("msg", "账户已过期,登陆失败!");
} else if (exception instanceof CredentialsExpiredException) {
result.put("msg", "密码已过期,登陆失败!");
} else {
result.put("msg", "登陆失败!");
}
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
};
}

/**
* 请求拒绝的处理器
* @return
*/
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return ((request, response, accessDeniedException) -> {
response.setContentType("application/json;charset=utf-8");
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.FORBIDDEN.value());
result.put("msg","权限不够");
response.getWriter().write(JSON.toJSONString(result));
}
);
}
}

3、测试Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class HelloController {

@GetMapping("/hello")
public String helle(){
return "hello";
}

@GetMapping("/getUserInfo")
public Object getUserInfo(Principal principal){
return principal;
}

@GetMapping("/getUserInfo2")
public Object getUserInfo(){
return SecurityContextHolder.getContext().getAuthentication();
}
}
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
@RestController
public class TestController {

/**
* 登录成功跳转 是一个post请求
* @return
*/
@PostMapping("welcome")
public String welcome() {
return "welcome";
}


@GetMapping("addUser")
@PreAuthorize("hasAuthority('sys:add')")
public String addUser() {
return "addUser";
}

@GetMapping("updateUser")
@PreAuthorize("hasAuthority('sys:update')")
public String updateUser() {
return "updateUser";
}

@GetMapping("delUser")
@PreAuthorize("hasAnyAuthority('sys:del')")
public String delUser() {
return "delUser";
}

@GetMapping("queryUser")
@PreAuthorize("hasAnyAuthority('sys:query')")
public String queryUser() {
return "queryUser";
}

@GetMapping("testUser")
@PreAuthorize("hasAnyAuthority('sys:test')")
public String testUser() {
return "testUser";
}

@GetMapping("free")
public String free() {
return "free";
}
}

4、从数据库中获取用户信息,作为校验

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
/**
* @Description 用户登录信息 实现SpringSecurity中的UserDetails接口
* @Author zhangyuzhen
* @Since JDK 1.8
* @Version V1.0
* @Date 2021/10/31 15:38
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserLoginInfo implements UserDetails {

private Integer userId;
private String userName;
private String userPwd;
private Integer status;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptyList();
}

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

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

@Override
public boolean isAccountNonExpired() {
return status==1;
}

@Override
public boolean isAccountNonLocked() {
return status==1;
}

@Override
public boolean isCredentialsNonExpired() {
return status==1;
}

@Override
public boolean isEnabled() {
return status==1;
}
}
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
/**
* @Description 从数据库中获取登录信息
* @Author zhangyuzhen
* @Since JDK 1.8
* @Version V1.0
* @Date 2021/10/31 15:26
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {

@DubboReference
private UserService userService;

@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
MyThreadLocal.userName.set(userName);
User user = userService.selectByName(userName);
// 转换对象
if (!ObjectUtil.isEmpty(user)) {
UserLoginInfo userLoginInfo = new UserLoginInfo();
userLoginInfo.setUserId(user.getId());
userLoginInfo.setUserName(user.getUsername());
userLoginInfo.setUserPwd(user.getPassword());
userLoginInfo.setStatus(user.getStatus());
return userLoginInfo;
}
return null;
}
}

使用Redis保存用户信息

在登录成功的处理方法successHandler中将token存入redis,并将token返回给前端

1
2
3
4
5
6
7
8
9
10
11
12
// 生成token 直接用uuid作为redis中用户信息的key
String token = UUID.randomUUID().toString().replace("-", "");
String value = JSON.toJSONString(authentication.getPrincipal());
stringRedisTemplate.opsForValue().set(LoginConstant.TOKEN_PREFIX+token,
JSON.toJSONString(authentication.getPrincipal()), 15 * 60, TimeUnit.SECONDS);
// 组装数据给前端
Map<String, Object> result = new HashMap<>();
result.put("status", 200);
result.put("token", token);
result.put("expire", 15 * 60);
result.put("msg", "登录成功");
response.getWriter().write(JSON.toJSONString(result));

定义过滤器实现token校验

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
/**
* @Description Token校验过滤器
* @Author zhangyuzhen
* @Since JDK 1.8
* @Version V1.0
* @Date 2021/10/31 16:49
*/
@Component
public class TokenCheckFilter extends OncePerRequestFilter {

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String path = request.getRequestURI();
// 放行
if (LoginConstant.ALLOW_URLS.contains(path)) {
filterChain.doFilter(request, response);
return;
}
// 校验
String header = request.getHeader(LoginConstant.HEAD_AUTHORIZATION);
if (StringUtils.hasText(header)) {
Boolean isExist = stringRedisTemplate.hasKey(LoginConstant.TOKEN_PREFIX+header);
if (null != isExist && isExist) {
JSONObject jsonObject = JSON.parseObject(stringRedisTemplate.opsForValue().get(LoginConstant.TOKEN_PREFIX + header));
String userName = jsonObject.getString("username");
// 封装成security内置的对象
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userName, null, Collections.emptyList());
authenticationToken.setDetails(new WebAuthenticationDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
return;
}
}
Map<String, Object> result = new HashMap<>();
result.put("status", 401);
result.put("msg", "验证失败");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
}
}