写点什么

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获取异步和图灵系列书籍

评论

发布
暂无评论
Spring 全家桶之 Spring Security(四)_8月月更_小白_InfoQ写作社区