步步深入Spring Security系列(四)

Chapter 05 自定义登录

Section 01 - 基于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
@EnableWebSecurity
public 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中定义的

image.png csrf().disable()则是禁用跨域访问的安全设置

以上配置完成之后,重新启动应用,使用三个用户Peter,Thor,Stark分别属于3个角色ADMIN,USER,READ,Thor账户还拥有ADMIN的权限,进行验证 使用Peter进行登录,只能访问ADMIN,其他报错

login.gif 权限功能正常,自定义登录页面适配成功。当用户名/密码输入错误时,页面仍停留在登录页面,因此可以自定义一个错误页面,当出现错误时跳转到错误页面,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

@Override
protected 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();
}
复制代码

重新启动应用,使用错误的用户名密码登录

error.gif

Section 02 - 基于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管理

@Component
public 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();
    }
}
复制代码
@Component
public 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
@EnableWebSecurity
public 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.gif 页面报错ajax请求状态为已取消,并且无法获得相应 image.png 解决这个问题的办法需要在ajax代码中增加一行代码,即可解决问题

async: false
复制代码

重新启动应用

ajax-success.gif

猜你喜欢

转载自juejin.im/post/7047406809551732749