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
@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 中定义的
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
@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();
}
重新启动应用,使用错误的用户名密码登录
基于 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 请求状态为已取消,并且无法获得相应
解决这个问题的办法需要在 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
@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");
// 设置返回的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();
}
}
@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");
// 设置返回的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获取异步和图灵系列书籍
评论