Spring 全家桶之 Spring Security(四)
- 2022 年 8 月 23 日 上海
本文字数:10187 字
阅读完需:约 33 分钟
一、自定义登录
基于 Form 表单登录
在 static 目录下增加登录界面 html
<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>登陆</title> <style> .main-body {top:50%;left:50%;position:absolute;-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);-o-transform:translate(-50%,-50%);transform:translate(-50%,-50%);overflow:hidden;} .login-main .login-bottom .center .item input {display:inline-block;width:227px;height:22px;padding:0;position:absolute;border:0;outline:0;font-size:14px;letter-spacing:0;} .login-main .login-bottom .tip .icon-nocheck {display:inline-block;width:10px;height:10px;border-radius:2px;border:solid 1px #9abcda;position:relative;top:2px;margin:1px 8px 1px 1px;cursor:pointer;} .login-main .login-bottom .center .item .icon {display:inline-block;width:33px;height:22px;} .login-main .login-bottom .center .item {width:288px;height:35px;border-bottom:1px solid #dae1e6;margin-bottom:35px;} .login-main {width:428px;position:relative;float:left;} .login-main .login-top {height:117px;background-color:#148be4;border-radius:12px 12px 0 0;font-family:SourceHanSansCN-Regular;font-size:30px;font-weight:400;font-stretch:normal;letter-spacing:0;color:#fff;line-height:117px;text-align:center;overflow:hidden;-webkit-transform:rotate(0);-moz-transform:rotate(0);-ms-transform:rotate(0);-o-transform:rotate(0);transform:rotate(0);} .login-main .login-top .bg1 {display:inline-block;width:74px;height:74px;background:#fff;opacity:.1;border-radius:0 74px 0 0;position:absolute;left:0;top:43px;} .login-main .login-top .bg2 {display:inline-block;width:94px;height:94px;background:#fff;opacity:.1;border-radius:50%;position:absolute;right:-16px;top:-16px;} .login-main .login-bottom {width:428px;background:#fff;border-radius:0 0 12px 12px;padding-bottom:53px;} .login-main .login-bottom .center {width:288px;margin:0 auto;padding-top:40px;padding-bottom:15px;position:relative;} .login-main .login-bottom .tip {clear:both;height:16px;line-height:16px;width:288px;margin:0 auto;} input::-webkit-input-placeholder {color:#a6aebf;} input::-moz-placeholder {/* Mozilla Firefox 19+ */ color:#a6aebf;} input:-moz-placeholder {/* Mozilla Firefox 4 to 18 */ color:#a6aebf;} input:-ms-input-placeholder {/* Internet Explorer 10-11 */ color:#a6aebf;} input:-webkit-autofill {/* 取消Chrome记住密码的背景颜色 */ -webkit-box-shadow:0 0 0 1000px white inset !important;} html {height:100%;} .login-main .login-bottom .tip {clear:both;height:16px;line-height:16px;width:288px;margin:0 auto;} .login-main .login-bottom .tip .login-tip {font-family:MicrosoftYaHei;font-size:12px;font-weight:400;font-stretch:normal;letter-spacing:0;color:#9abcda;cursor:pointer;} .login-main .login-bottom .tip .forget-password {font-stretch:normal;letter-spacing:0;color:#1391ff;text-decoration:none;position:absolute;right:62px;} .login-main .login-bottom .login-btn {width:288px;height:40px;background-color:#1E9FFF;border-radius:16px;margin:24px auto 0;text-align:center;line-height:40px;color:#fff;font-size:14px;letter-spacing:0;cursor:pointer;border:none;} .login-main .login-bottom .center .item .validateImg {position:absolute;right:1px;cursor:pointer;height:36px;border:1px solid #e6e6e6;} .footer {left:0;bottom:0;color:#fff;width:100%;position:absolute;text-align:center;line-height:30px;padding-bottom:10px;text-shadow:#000 0.1em 0.1em 0.1em;font-size:14px;} .padding-5 {padding:5px !important;} .footer a,.footer span {color:#fff;} @media screen and (max-width:428px) {.login-main {width:360px !important;} .login-main .login-top {width:360px !important;} .login-main .login-bottom {width:360px !important;} } </style></head><body><div class="main-body"> <div class="login-main"> <div class="login-top"> <span>Login</span> <span class="bg1"></span> <span class="bg2"></span> </div> <form class="layui-form login-bottom" method="post" action="/login"> <div class="center"> <div class="item"> <span class="icon icon-2"></span> <input type="text" name="username" lay-verify="required" placeholder="请输入登录账号" maxlength="24"/> </div>
<div class="item"> <span class="icon icon-3"></span> <input type="password" name="password" lay-verify="required" placeholder="请输入密码" maxlength="20"> <span class="bind-password icon icon-4"></span> </div> <div class="layui-form-item" style="text-align:center; width:100%;height:100%;margin:0px;"> <button class="login-btn" lay-submit="" lay-filter="login" type="submit">立即登录</button> </div> </div> </form> </div></div></body></html>
修改自定义安全配置类
@Configuration@EnableWebSecuritypublic class CustSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource private UserDetailsService userDetailsService;
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()); }
@Override protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 配置任意都可以访问的界面 .antMatchers("/index.html","/login.html","/login").permitAll() // 给url配置角色访问权限 .antMatchers("/access/user").hasRole("USER") .antMatchers("/access/read").hasRole("READ") .antMatchers("/access/admin").hasRole("ADMIN") .anyRequest().authenticated() .and() .formLogin() // 指定使用自定义的登录界面 .loginPage("/login.html") .loginProcessingUrl("/login") // 指定登录地址 .and() .csrf().disable(); }}
首先将新增的 login.html 页面配置到 permitAll 项上表示任意用户都可以访问的界面,无需权限,登录 url 地址"/login"同样也是无需任何权限即可访问,并且使用 loginPage 方法指定使用自定义的登录界面,loginProcessingUrl 指定使用的登录地址,“./login” URL 地址是 Spring Security 内置的登录地址,在过滤器 UsernamePasswordAuthenticationFilter 中定义的
csrf().disable()则是禁用跨域访问的安全设置
以上配置完成之后,重新启动应用,使用三个用户 Peter,Thor,Stark 分别属于 3 个角色 ADMIN,USER,READ,Thor 账户还拥有 ADMIN 的权限,进行验证使用 Peter 进行登录,只能访问 ADMIN,其他报错
权限功能正常,自定义登录页面适配成功。当用户名/密码输入错误时,页面仍停留在登录页面,因此可以自定义一个错误页面,当出现错误时跳转到错误页面,404 页面代码如下
<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>Error</title> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="Access-Control-Allow-Origin" content="*"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="format-detection" content="telephone=no"> <style> .error .clip .shadow {height:180px;} .error .clip:nth-of-type(2) .shadow {width:130px;} .error .clip:nth-of-type(1) .shadow,.error .clip:nth-of-type(3) .shadow {width:250px;} .error .digit {width:150px;height:150px;line-height:150px;font-size:120px;font-weight:bold;} .error h2 {font-size:32px;} .error .msg {top:-190px;left:30%;width:80px;height:80px;line-height:80px;font-size:32px;} .error span.triangle {top:70%;right:0%;border-left:20px solid #535353;border-top:15px solid transparent;border-bottom:15px solid transparent;} .error .container-error-404 {top: 50%;margin-top: 250px;position:relative;height:250px;padding-top:40px;} .error .container-error-404 .clip {display:inline-block;transform:skew(-45deg);} .error .clip .shadow {overflow:hidden;} .error .clip:nth-of-type(2) .shadow {overflow:hidden;position:relative;box-shadow:inset 20px 0px 20px -15px rgba(150,150,150,0.8),20px 0px 20px -15px rgba(150,150,150,0.8);} .error .clip:nth-of-type(3) .shadow:after,.error .clip:nth-of-type(1) .shadow:after {content:"";position:absolute;right:-8px;bottom:0px;z-index:9999;height:100%;width:10px;background:linear-gradient(90deg,transparent,rgba(173,173,173,0.8),transparent);border-radius:50%;} .error .clip:nth-of-type(3) .shadow:after {left:-8px;} .error .digit {position:relative;top:8%;color:white;background:#1E9FFF;border-radius:50%;display:inline-block;transform:skew(45deg);} .error .clip:nth-of-type(2) .digit {left:-10%;} .error .clip:nth-of-type(1) .digit {right:-20%;} .error .clip:nth-of-type(3) .digit {left:-20%;} .error h2 {font-size:24px;color:#A2A2A2;font-weight:bold;padding-bottom:20px;} .error .tohome {font-size:16px;color:#07B3F9;} .error .msg {position:relative;z-index:9999;display:block;background:#535353;color:#A2A2A2;border-radius:50%;font-style:italic;} .error .triangle {position:absolute;z-index:999;transform:rotate(45deg);content:"";width:0;height:0;} @media(max-width:767px) {.error .clip .shadow {height:100px;} .error .clip:nth-of-type(2) .shadow {width:80px;} .error .clip:nth-of-type(1) .shadow,.error .clip:nth-of-type(3) .shadow {width:100px;} .error .digit {width:80px;height:80px;line-height:80px;font-size:52px;} .error h2 {font-size:18px;} .error .msg {top:-110px;left:15%;width:40px;height:40px;line-height:40px;font-size:18px;} .error span.triangle {top:70%;right:-3%;border-left:10px solid #535353;border-top:8px solid transparent;border-bottom:8px solid transparent;} .error .container-error-404 {height:150px;} } </style></head><body><div class="error"> <div class="container-floud"> <div style="text-align: center"> <div class="container-error-404"> <div class="clip"> <div class="shadow"> <span class="digit thirdDigit"></span> </div> </div> <div class="clip"> <div class="shadow"> <span class="digit secondDigit"></span> </div> </div> <div class="clip"> <div class="shadow"> <span class="digit firstDigit"></span> </div> </div> <div class="msg">OH! <span class="triangle"></span> </div> </div> <h2 class="h1">很抱歉,你访问的页面找不到了</h2> </div> </div></div><script> function randomNum() { return Math.floor(Math.random() * 9) + 1; }
var loop1, loop2, loop3, time = 30, i = 0, number; loop3 = setInterval(function () { if (i > 40) { clearInterval(loop3); document.querySelector('.thirdDigit').textContent = 4; } else { document.querySelector('.thirdDigit').textContent = randomNum(); i++; } }, time); loop2 = setInterval(function () { if (i > 80) { clearInterval(loop2); document.querySelector('.secondDigit').textContent = 0; } else { document.querySelector('.secondDigit').textContent = randomNum(); i++; } }, time); loop1 = setInterval(function () { if (i > 100) { clearInterval(loop1); document.querySelector('.firstDigit').textContent = 4; } else { document.querySelector('.firstDigit').textContent = randomNum(); i++; } }, time);</script></body></html>
修改自定义安全配置 CustSecurityConfig
@Overrideprotected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() .antMatchers("/index.html","/login.html","/login","/404.html").permitAll() // 给url配置角色访问权限 .antMatchers("/access/user").hasRole("USER") .antMatchers("/access/read").hasRole("READ") .antMatchers("/access/admin").hasRole("ADMIN") .anyRequest().authenticated() .and() .formLogin() // 指定使用自定义的登录界面 .loginPage("/login.html") .loginProcessingUrl("/login") .failureUrl("/404.html") // 指定跳转的错误页面 .and() .csrf().disable();}
重新启动应用,使用错误的用户名密码登录
基于 AJAX 登录
在登录页面增加 ajax 代码
<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>登陆</title> <style> //此处省略了样式代码,样式没有任何改变 </style> <script type="text/javascript" src="/js/jquery-3.4.1.js"></script> <script type="text/javascript"> $(function(){ //juqery的入口函数 $("#btnLogin").click(function(){ var uname = $("#username").val(); var pwd = $("#password").val(); $.ajax({ url:"/login", type:"POST", data:{ "username":uname, "password":pwd }, dataType:"json", success:function(resp){ alert(resp.msg) } }) }) }) </script></head><body><div class="main-body"> <div class="login-main"> <div class="login-top"> <span>Login</span> <span class="bg1"></span> <span class="bg2"></span> </div> <form class="layui-form login-bottom"> <div class="center"> <div class="item"> <span class="icon icon-2"></span> <input type="text" id="username" lay-verify="required" placeholder="请输入登录账号" maxlength="24"/> </div>
<div class="item"> <span class="icon icon-3"></span> <input type="password" id="password" lay-verify="required" placeholder="请输入密码" maxlength="20"> <span class="bind-password icon icon-4"></span> </div> <div class="layui-form-item" style="text-align:center; width:100%;height:100%;margin:0px;"> <button class="login-btn" lay-submit="" lay-filter="login" id="btnLogin">立即登录</button> </div> </div> </form> </div></div></body></html>
取消了 form 表单提交数据,增加了 ajax 代码,并给 username 和 password 以及登录按钮增加了 id 属性,通过 ajax 代码获取属性的 value,向后端发送 POST 请求新增 handler 包,增加 successHandler 即校验用户名密码成功后后执行的 handler,及 faliureHandler 校验密码失败后执行的 handler,增加 @Component 属性,将这两个 Handler 交割 Spring 管理
@Componentpublic class CustSuccessHandler implements AuthenticationSuccessHandler {
@Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 登录的用户信息验证成功后执行的方法 response.setContentType("text/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.println("{"msg":"登录成功"}"); writer.flush(); writer.close(); }}
@Componentpublic class CustFailureHandler implements AuthenticationFailureHandler {
@Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { // 验证登录信息失败后执行的方法 response.setContentType("text/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.println("{"msg":"验证失败"}"); writer.flush(); writer.close(); }}
修改自定义安全配置
@Configuration@EnableWebSecuritypublic class CustSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource private UserDetailsService userDetailsService;
@Resource private CustSuccessHandler custSuccessHandler;
@Resource private CustFailureHandler custFailureHandler;
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()); }
@Override protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 增加js静态资源的访问权限 .antMatchers("/login.html","/index.html","/login","/js/**").permitAll() // 给url配置角色访问权限 .antMatchers("/access/user").hasRole("USER") .antMatchers("/access/read").hasRole("READ") .antMatchers("/access/admin").hasRole("ADMIN") .anyRequest().authenticated() .and() .formLogin() .successHandler(custSuccessHandler) //执行验证成功的handler .failureHandler(custFailureHandler) // 执行验证失败后的handler // 指定使用自定义的登录界面 .loginPage("/login.html") .loginProcessingUrl("/login") .and() .csrf().disable(); }}
使用 @Resource 注解将两个 Handler 注入,并且增加了 js 访问的白名单以及配置了验证成功和失败后的处理器
重启应用,并访问,如果页面显示加载 jQyery 失败,可以在 Idea 上 Rebuild 一下
页面报错 ajax 请求状态为已取消,并且无法获得相应
解决这个问题的办法需要在 ajax 代码中增加一行代码,即可解决问题
async: false
重新启动应用
返回 JSON 格式数据
增加 jackson 依赖
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.9.8</version></dependency><dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.8</version></dependency>
新增 common 包,增加一个 Result 类,用来表示返回的 JSON 格式的数据
public class Result {
private int code; private int error; private String msg; // 此处省略getter/setter方法}
改造 CustSuccessHandler 和 CustFailureHandler
@Componentpublic class CustSuccessHandler implements AuthenticationSuccessHandler {
@Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 登录的用户信息验证成功后执行的方法 response.setContentType("text/json;charset=utf-8"); // 设置返回的Json格式的结果 Result result = new Result(); result.setCode(0); result.setError(1000); result.setMsg("登录成功"); OutputStream outputStream = response.getOutputStream(); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.writeValue(outputStream,result); outputStream.flush(); outputStream.close();
}}
@Componentpublic class CustFailureHandler implements AuthenticationFailureHandler {
@Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { // 验证登录信息失败后执行的方法 response.setContentType("text/json;charset=utf-8"); // 设置返回的Json格式的结果 Result result = new Result(); result.setCode(0); result.setError(1001); result.setMsg("登录失败"); OutputStream outputStream = response.getOutputStream(); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.writeValue(outputStream,result); outputStream.flush(); outputStream.close(); }}
重启应用,即可返回 Json 格式数据
小白
QA 2019.08.05 加入
微信号JingnanSJ或者公众号RiemannHypo获取异步和图灵系列书籍









评论