Spring Security-3-自定义登录页

上文中实现了用户自定义的登录认证,但是用的登录页面是默认的登录页,在实际开发中,当然不可能用这种登录页,所以还是需要我们自己去搞一个登录的页面的, 还有一个问题就是,现在大部分开发都是前后端分离的, 如果是针对App的接口的话,我们直接返回一个html页面是不可以的,应该是返回一个json串给前端,然后前端去引导用户到登录页

然后还有一个问题,先来看一下现有的项目结构

┌security-example-app
├security-example-web
├security-example-core
└security-example-demo

这里core是放一些核心的代码,然后web是针对浏览器的配置,app是针对app的配置,demo是用来调用上面的服务测试的一个工程
然后我们还要实现的一个功能是,如果用户在demo中配置了登录页,就用用户配置的登录页,如果没有,我们给一个默认的登录页

主要功能

  1. 自定义登录页面
  2. 区分是浏览器请求,还是其他请求, 如果是浏览器请求的话,就返回登录页面,如果是其他的,就返回json信息
  3. 配置可以被调用服务者覆盖的一个登录页

自定义登录页

首先先来看一下如何实现自定义的登录页,首先随便写一个登录的页面,或者网上下载一个,复制到web工程里面,作为我们默认的登录页

如图,我随便找了一个登录页放到项目里

image

resources/static,这个目录也不需要额外的做什么配置,springboot默认配置好的

然后在之前创建的 BrowserSecurityConfig 里面,来配置默认的登录页,修改后的代码如下:

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

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

@Override
protected void configure(HttpSecurity http) throws Exception {
// formLogin 表示表单认证
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/authentication/from")
.and()
// 授权请求. anyRequest 就表示所有的请求都需要权限认证
.authorizeRequests()
// 匹配的是登录页的话放行
.antMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();

}

}

简单的看一下这里的代码

  • .loginPage("/login.html")指定了登录的跳转页面
  • .loginProcessingUrl("/authentication/from") 配置要处理的登录请求,这个要和html表单的 action 对应,默认是/login
  • .antMatchers("/login.html").permitAll() 这里配置是配置登录页不需要权限,否则跳转登录页的时候也做权限验证,那就进不去了
  • .and().csrf().disable(); 这个配置是防止csrf攻击的,先关掉,否则会报错,具体原因后面再说

配置都ok之后,启动项目,访问一个接口,就能看到我们自己的登录页了

区分请求来源

自定义登录页面完成之后,再看一下如何区分是浏览器发起的请求,还是其他发起的请求,然后先来看一下大致的思路,如图:
image

首先请求过来之后,如果需要认证,就跳转到一个我们自定义的controller,然后在这个controller中去判断请求的来源,做出不同的响应

上面我们请求到达需要认证的话,就直接跳转到登录页了,然后现在我们写一个controller去区分,他是来自网页的请求,还是app的或者其他地方的

创建一个controller, BrowserController 代码如下:

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
@Slf4j
@RestController
@RequestMapping("/authentication/require")
public class BrowserController {

private RequestCache requestCache = new HttpSessionRequestCache();

private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

/**
* 需要身份认证的时候先跳转到这里
* @param request
* @param response
* @return
*/
@RequestMapping(produces = "text/html")
public void requireAuthenticationHtml(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);

if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
log.info("引发跳转的请求是:" + targetUrl);
redirectStrategy.sendRedirect(request, response, "/login.html");
}
}

@RequestMapping
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Map<String, Object> requireAuthenticationJson() {
Map<String, Object> resultMap = new HashMap<>(1);
resultMap.put("message", "认证失败,请登录...");
return resultMap;
}

}

上面的方法 就是看请求头里面有没有 text/html, 有的话就进, 没有的话就进到了下面的方法,下面的方法中返回了一个 401 的状态码,就表示没有认证,然后是一个json的提示信息
上面的方法中,有一个 RequestCache security会把当前的请求放到这个session里面,然后 RedirectStrategy 是一个重定向的工具类, 这个方法就是要重定向一个登录页, 这里是直接跳转到了 /login.html

最后再把配置类中的登录页的路径修改掉,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void configure(HttpSecurity http) throws Exception {
// formLogin 表示表单认证
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/from")
.and()
.authorizeRequests()
// 匹配的是登录页的话放行
.antMatchers("/authentication/require", "/login.html").permitAll()
// 授权请求. anyRequest 就表示所有的请求都需要权限认证
.anyRequest().authenticated()
.and().csrf().disable();

}

可覆盖登录页配置

最后一个问题, 我们上面的登录页是写死的, 我们要提供一个可配置的能力,就是在demo项目中可以覆盖掉我们现在的登录页, 使用用户自定义的登录页

这里就可以用配置类去解决了,这里我在core工程中创建了以下三个类

1
2
3
4
5
6
7
8
9
10
@Data
@ConfigurationProperties(prefix = "core.security")
public class SecurityProperties {

/**
* 浏览器配置
*/
private BrowserProperties browser = new BrowserProperties();

}

1
2
3
4
5
6
@Data
public class BrowserProperties {

private String loginPage = "/login.html";

}
1
2
3
4
@Configuration
@EnableConfigurationProperties({SecurityProperties.class})
public class SecurityConfig {
}

第一个是一个配置的总类,设置了前缀是 core.security, 里面又放了一个浏览器的配置类,因为可能还有其他的配置, 最后一个是用来启用这个配置类的

这些配置完成之后,我们就可以在demo项目中的 application.yml 中注入这些属性的值了

比如上面的登录页路径,在 application.yml 中加入以下配置:

1
2
3
4
core:
security:
browser:
loginPage: /myLogin.html

然后demo项目中也需要一个登录的html页面,我直接复制过来,修改了title和名称,区分一下.

测试

到这里本文需要实现的功能就都完成了,然后做一下测试看看实际的效果

先来看一下两个登录页,首先是我们的web项目提供的,如下:
image

然后是用户自定义的:
image

这里的title和文件名是不一样的(文件名相同的话,不用配置就会直接覆盖的)

然后先用默认的试一下,也就是在 application.yml 里面不做任何配置,启动项目,然后访问一个接口,效果如下:
image

可以看到,不加配置的时候是跳转到了我们默认的页面的

然后再加上自定义的配置,重启项目再访问一次,这时候就会跳转到我们的自定义的登录页
image

最后用postMan再访问一次,看一下返回的是什么 image

这里不是从浏览器进去的请求,所以返回的是一个json的提醒,还有一个401的状态码.

总结

主要是做了自定义的登录页跳转的配置, 然后是根据不同环境返回不同的信息,最后实现了动态配置登录页,源码已经上传github,传送门