Blog

  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

springboot-security

发表于 2019-08-27 更新于 2019-08-28 分类于 SpringBoot 阅读次数:
本文字数: 15k

基本原理

SpringSecurity 最核心的东西 其实是一个过滤器链,一组Filter
所有发送的请求都会经过Filter链,同样响应也会经过Filter链,在系统启动的时候springboot会自动的把他们配置进去

代码

启动器

1
2
3
4
5
6
7
8
<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>

自定义配置类

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
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private MyProvider provider;
@Autowired
private MySuccessHandler mySuccessHandler;
@Autowired
private MyFailureHandler myFailureHandler;
@Autowired
private MyDeniedHandler myDeniedHandler;
@Autowired
private MyEntryPoint myEntryPoint;

// @Override
// protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth
// .inMemoryAuthentication()
// .passwordEncoder(new BCryptPasswordEncoder())
// .withUser("user1")
// .password(new BCryptPasswordEncoder().encode("123456"))
// .roles("user","admin");
// }

//使用自定义的安全认证(provider)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(provider);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
//设置过滤路径(ant风格)以及所需要的权限
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/user/**").hasRole("admin")
.antMatchers("/test/**").permitAll()
.antMatchers("/").authenticated()
.and()
//使用表单登录,httpBasic()的话是使用弹窗
.formLogin()
//自定义登录页面
.loginPage("/login")
//自定义登录处理url
.loginProcessingUrl("/mylogin")
//自定义登录成功处理器
.successHandler(mySuccessHandler)
//自定义登录失败处理器
.failureHandler(myFailureHandler)
.and()
//关闭csrf(暂时)
.csrf().disable();
http.exceptionHandling()
//自定义权限不足处理器
.accessDeniedHandler(myDeniedHandler)
//自定义未登录处理器
.authenticationEntryPoint(myEntryPoint);
}
}

MyDeniedHandler

1
2
3
4
5
6
7
@Component
public class MyDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.getWriter().write("no permission");
}
}

MyEntryPoint

1
2
3
4
5
6
7
8
@Component
public class MyEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.getWriter().write("no login");
}
}

MySuccessHandler

1
2
3
4
5
6
7
@Component
public class MySuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.getWriter().write("success");
}
}

MyFailureHandler

1
2
3
4
5
6
7
@Component
public class MyFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.getWriter().write("fail");
}
}

MyProvider

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
@Component
public class MyProvider implements AuthenticationProvider {

@Autowired
private MyUserDetailService userDetailService;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//获取表单输入的用户名
String username = (String) authentication.getPrincipal();
//获取密码
String password = (String) authentication.getCredentials();

UserDetails userDetails = userDetailService.loadUserByUsername(username);

if(!userDetails.getPassword().equals(password))
{
throw new BadCredentialsException("密码错误");
}

return new UsernamePasswordAuthenticationToken(username,userDetails.getPassword(),userDetails.getAuthorities());


}

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

MyUserDetailService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通过用户名获取用户信息
//封装为一个UserDetails对象,User是UserDeatils的一个实现类

//模拟从数据库查询出来的密码和角色
String password = "123456";
User user = new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_ADMIN"));
return user;

}
}

权限判断过程分析

  • 用户发送请求。
  • 调用UsernamePasswordAuthenticationFilter中的doFilter(父类AbstractAuthenticationProcessing中的doFilter)
  • 调用ExceptionTranslationFilter中的doFilter
  • 调用FilterSecurityInterceptor中的doFilter
    • 调用doFilter中的invoke
      • invoke中的beforeInvocation
        1. 从security容器中获取authentication
        2. 判断authentication是否具有权限
          • 有:继续执行fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
          • 没有:
            • 没有登录:调用authenticationEntryPoint的commence方法,默认是DelegatingAuthenticationEntryPoint,会重定向到默认的login页面。登录时:
              1. 调用UsernamePasswordAuthenticationFilter中的attemptAuthentication,获取username和password,获取一个AuthenticationManager,并调用其中的authentication方法(其实就是遍历AuthenticatinProvider,并调用其中的authentication方法),在方法中判断用户名和密码是否正确。
                • 正确:调用successHandler中的onAuthenticationSuccess
                • 错误:调用failureHandler的onAuthenticationFailure方法
            • 权限不足:调用AccessDeniedHandler中的handle方法,默认是AccessDeniedHandlerImpl,会转发到errorPage。

权限判断过程源码

AbstractAuthenticationProcessingFilter

1
2
3
4
5
6
7
8
9
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
//...
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);

return;
}
//....

调用chain.doFilter(),然后调用ExceptionTranslationFilter中的doFilter

ExceptionTranslationFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
try {
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
//...
handleSpringSecurityException(request, response, chain, ase);
}
}

FilterSecurityInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}

public void invoke(FilterInvocation fi) throws IOException, ServletException {
//...
InterceptorStatusToken token = super.beforeInvocation(fi);

try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}

super.afterInvocation(token, null);
}
}

这里有个重要的方法

1
InterceptorStatusToken token = super.beforeInvocation(fi);

这个方法是用来判断是否具有权限的地方;如果有权限的话,就会返回一个token,并执行下面的doFilter方法,也就是业务代码;
如果没有权限的话,就是抛出一个异常。此异常会在ExceptionTranslationFilter中被捕获,并执行 handleSpringSecurityException(request, response, chain, ase)方法

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
private void handleSpringSecurityException(HttpServletRequest request,
//...
if (exception instanceof AuthenticationException) {
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}

protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}

这里首先会判断一下异常的类型:

  • 如果是AuthenticationException异常的话就会调用sendStartAuthentication方法,
    这个方法中会调用authenticationEntryPoint.commence(request, response, reason);这个authenticationEntryPoint可以自定义,默认是DelegatingAuthenticationEntryPoint,
    其中的commence方法会自动redirect到登陆页面。
  • 如果是AccessDeniedException异常的话,会先在security容器中取出认证信息(即当前用户信息):
    • 如果是匿名用户(即没有登录),就调用sendStartAuthentication方法,重定向到登录界面。
    • 如果不是匿名用户,则说明是权限不足,就会调用accessDeniedHandler中的handle方法。默认使用的是AccessDeniedHandlerImpl。会转发到errorPage,
      可以自定义。

如果是没有登录,跳转到了登录页面的话(可以自定义登录页面,给前端返回json数据,前端发送ajax请求的地址是设置的loginProcessUrl(默认是/login,可以自定义))
当前端提交了用户名和密码之后,会调用attemptAuthentication方法

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
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
unsuccessfulAuthentication(request, response, failed);

return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);

return;
}

// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}

successfulAuthentication(request, response, chain, authResult);

attemptAuthentication方法是在UsernamePasswordAuthenticationFilter中实现。

UsernamePasswordAuthenticationFilter

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
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
}

这里会获取request中的username和password(默认的表单中的name也是username和password,可以自定义,在configure中设置usernameParamter)

1
2
3
4
5
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

然后就会获取一个AuthenticationManager,并调用其中的authenticate方法。

1
2
3
4
5
6
7
8
9
for (AuthenticationProvider provider : getProviders()) {
try {
result = provider.authenticate(authentication);

if (result != null) {
copyDetails(authentication, result);
break;
}
}

其实就是遍历AuthenticationProvier,并调用其中的authenticate方法,provider可以我们自定义。

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
@Component
public class MyProvider implements AuthenticationProvider {

@Autowired
private MyUserDetailService userDetailService;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//获取表单输入的用户名
String username = (String) authentication.getPrincipal();
//获取密码
String password = (String) authentication.getCredentials();

UserDetails userDetails = userDetailService.loadUserByUsername(username);

if(!userDetails.getPassword().equals(password))
{
throw new BadCredentialsException("密码错误");
}

return new UsernamePasswordAuthenticationToken(username,password,userDetails.getAuthorities());
}

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

这里就是认证用户的地方,校验用户名密码,获取权限。其中的UserDetails就是封装
用户信息的一个类,通过UserDetailsService.loadUserByUsername方法获取。UserDetailsService是一个接口,需要自定义实现类

MyUserDetailService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通过用户名获取用户信息
//封装为一个UserDetails对象,User是UserDeatils的一个实现类
//假装查询出来的
String password = "123456";

User user = new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_user,admin"));
return user;

}
}

这里就是更具用户名从数据库中获取密码和权限。
然后在provider中进行校验密码。如果密码错误,或者用户名不存在,就抛出特定的异常。
异常会在AbstractAuthenticationProcessingFilter中捕获(AbstractAuthenticationProcessingFilter->UsernamePasswordAuthenticationFilter
->MyProvider)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);

return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);

return;
}

spring security给我们准备了很多的异常。如下图

如果抛出了用户名和密码的异常,就会调用AbstractAuthenticationProcessingFilter中的unsuccessfulAuthentication(request, response, failed);
最后会调用failureHandler的onAuthenticationFailure方法。
默认使用SimpleUrlAuthenticationFailureHandler,这个failureHandler可自定义。

1
2
3
4
5
6
7
@Component
public class MyFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.getWriter().write("fail");
}
}

如果用户名和密码没有问题,就会返回一个Authentication,然后调用successHandler中的onAuthenticationSuccess
默认使用SavedRequestAwareAuthenticationSuccessHandler,可以自定义

1
2
3
4
5
6
7
@Component
public class MySuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.getWriter().write("success");
}
}

登录后,用户认证信息就是保存在security容器中,当再次访问时,就会判断权限是否足够。

github

gitee


------ 已触及底线感谢您的阅读 ------
麻辣香锅不要辣 微信支付

微信支付

  • 本文作者: 麻辣香锅不要辣
  • 本文链接: https://http://ybhub.gitee.io/2019/08/27/springboot-security/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
# SpringBoot # Security
springboot-rabbitmq
springboot_jpa
  • 文章目录
  • 站点概览
麻辣香锅不要辣

麻辣香锅不要辣

21 日志
11 分类
20 标签
GitHub 简书
  1. 1. 基本原理
  2. 2. 代码
    1. 2.1. 启动器
    2. 2.2. 自定义配置类
    3. 2.3. MyDeniedHandler
    4. 2.4. MyEntryPoint
    5. 2.5. MySuccessHandler
    6. 2.6. MyFailureHandler
    7. 2.7. MyProvider
    8. 2.8. MyUserDetailService
  3. 3. 权限判断过程分析
  4. 4. 权限判断过程源码
    1. 4.1. AbstractAuthenticationProcessingFilter
    2. 4.2. ExceptionTranslationFilter
    3. 4.3. FilterSecurityInterceptor
    4. 4.4. UsernamePasswordAuthenticationFilter
    5. 4.5. MyUserDetailService
© 2019 – 2020 麻辣香锅不要辣 | 站点总字数: 20.4k字
|
0%