SpringBoot+Vue实现SpringSecurity+验证码登录与点击刷新验证码

SpringBoot+Vue实现SpringSecurity+验证码登录与点击刷新验证码

效果图

总体页面

表单填写完成在这里插入图片描述

验证码错误在这里插入图片描述

密码或用户名错误

登录成功

后端

建表SQL

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
CREATE TABLE `admin` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户自增Id',
`username` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '用户登录名',
`password` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '用户登录密码',
`isEnable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '用户是否被禁用(1:禁用,0:启用)',
`name` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '真实姓名',
`avatar` varchar(300) COLLATE utf8_bin DEFAULT NULL COMMENT '用户头像',
`phone` varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT '用户电话',
`QQ` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'QQ',
`wechat` varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT '微信',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
insert into `admin`(`id`,`username`,`password`,`isEnable`,`name`,`avatar`,`phone`,`QQ`,`wechat`) values
(1,'admin','$2a$10$7PMd9hwuYYe6.8rgmfn1x.GbrS4FyuH1nf80xb45zqm/ntZyzccBG',1,'张三','密码为111,这个字段是给头像用的','111','2548841623','微信'),
(2,'admins','$2a$10$k.uVP6DEc/X.DceezRIi1.zALL6rhsMpCVZFbt6pZDppdFV3wLIWi',1,'李四','密码为111,这个字段是给头像用的','111','111','111');
CREATE TABLE `admin_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户表与角色表的关联表自增ID',
`adminID` int(11) NOT NULL COMMENT '对应用户表的ID',
`roleID` int(11) NOT NULL COMMENT '对于角色表的ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
insert into `admin_role`(`id`,`adminID`,`roleID`) values (1,1,1);
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色表自增Id',
`name` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '角色的英文名,给电脑用的',
`nameZH` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '角色中文名,给用户看的',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
insert into `role`(`id`,`name`,`nameZH`) values
(1,'ROLE_system','系统管理员'),
(2,'ROLE_admin','用户'),
(3,'ROLE_user','普通用户');
环境(POM.xml)
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
36
37
38
39
40
41
42
43
44
45
46
<!-- SPringBoot版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<!-- web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Durid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.14</version>
</dependency>

<!-- 验证码 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>

<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.5.1</version>
</dependency>

<!-- M -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

实体类

用户实体类Admin(主要实现七个方法)

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
36
37
38
39
40
41
42
43
44
45
46
47
public class Admin implements UserDetails{//实现UserDetails,让springsecurity接管用户对象
private static final long serialVersionUID = 1L;
private Integer id;//id
private String username;//用户名
private String password;//密码
private Boolean isEnable;//账号禁用与否
private List<Role> roles;//角色集合
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> simpleGrantedAuthority = new ArrayList<>();
for(Role role:roles) {
simpleGrantedAuthority.add(new SimpleGrantedAuthority(role.getName()));
}
return simpleGrantedAuthority;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return isEnable;
}

@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return username;
}
//.....getter setter
}

权限实体类(Role)

1
2
3
4
5
6
public class Role {
private Integer id;
private String name;
private String nameZH;
//.....getter setter
}

用户角色关联实体类(AdnimRole)

1
2
3
4
5
6
public class AdminRole {
private Integer id;
private Integer adminID;
private Integer roleID;
//.....getter setter
}

Mapper

AadminMapper.java

1
2
3
4
5
@Mapper
public interface AdminMapper {
Admin loadUserByUsername(@Param("username")String username);//查询用户名
List<Role> getAdminRoleById(@Param("id")Integer id);//查询用户权限
}

AdminMapper.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xyz.nieqiang.webapp.mapper.AdminMapper">
<resultMap id="BaseResultMap" type="xyz.nieqiang.webapp.entity.Admin">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="password" jdbcType="VARCHAR" property="password" />
<result column="isEnable" jdbcType="BIT" property="isEnable" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="QQ" jdbcType="VARCHAR" property="QQ" />
<result column="wechat" jdbcType="VARCHAR" property="wechat" />
<result column="avatar" jdbcType="VARCHAR" property="avatar" />
<result column="phone" jdbcType="VARCHAR" property="phone" />
<collection property="roles" javaType="xyz.nieqiang.webapp.entity.Role">
<id column="rid" jdbcType="INTEGER" property="id"/>
<result column="rname" jdbcType="VARCHAR" property="name"/>
<result column="rnameZh" jdbcType="VARCHAR" property="nameZh"/>
</collection>
</resultMap>
<sql id="Base_Column_List">
id, username, password, avatar, phone, isEnable, QQ, wechat, name
</sql>

<!-- 登录关键查询 -->
<select id="loadUserByUsername" resultType="xyz.nieqiang.webapp.entity.Admin">
SELECT <include refid="Base_Column_List"></include> FROM admin WHERE username=#{username}
</select>

<!-- 角色获取关键查询 -->
<select id="getAdminRoleById" resultType="xyz.nieqiang.webapp.entity.Role">
SELECT r.* FROM role r, admin_role ar WHERE ar.adminID=#{id} AND ar.roleID=r.id
</select>
</mapper>

serviceImpl

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
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import cn.hutool.core.util.StrUtil;
import xyz.nieqiang.webapp.entity.Admin;
import xyz.nieqiang.webapp.mapper.AdminMapper;
import xyz.nieqiang.webapp.service.AdminService;
import xyz.nieqiang.webapp.util.UserIPUtil;
@Service
public class AdminServiceImpl implements UserDetailsService{
private static final Logger logger = LoggerFactory.getLogger(AdminServiceImpl.class);

@Autowired
private AdminMapper adminMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Admin admin = adminMapper.loadUserByUsername(username);
if(admin == null) {
throw new UsernameNotFoundException(username);
}
admin.setRoles(adminMapper.getAdminRoleById(admin.getId()));
logger.info("{}登录了",username);
return admin;
}
}

验证码配置

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.util.Properties;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;

@Configuration
public class KaptchaConfiguration {

@Bean
public DefaultKaptcha producer() {
Properties properties = new Properties();//是否有边框
properties.put("kaptcha.border", "no");
properties.put("kaptcha.border.color", "red");
properties.put("kaptcha.textproducer.font.color", "black");
properties.put("kaptcha.textproducer.char.space", "1");
properties.put("kaptcha.textproducer.char.length", "4");//验证码文本字符长度 默认为5
properties.put("kaptcha.image.width", "120");
properties.put("kaptcha.image.height", "50");
properties.put("kaptcha.textproducer.font.size", "40");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}

// kaptcha.border 是否有边框 默认为true 我们可以自己设置yes,no
// kaptcha.border.color 边框颜色 默认为Color.BLACK
// kaptcha.border.thickness 边框粗细度 默认为1
// kaptcha.producer.impl 验证码生成器 默认为DefaultKaptcha
// kaptcha.textproducer.impl 验证码文本生成器 默认为DefaultTextCreator
// kaptcha.textproducer.char.string 验证码文本字符内容范围 默认为abcde2345678gfynmnpwx
// kaptcha.textproducer.char.length 验证码文本字符长度 默认为5
// kaptcha.textproducer.font.names 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
// kaptcha.textproducer.font.size 验证码文本字符大小 默认为40
// kaptcha.textproducer.font.color 验证码文本字符颜色 默认为Color.BLACK
// kaptcha.textproducer.char.space 验证码文本字符间距 默认为2
// kaptcha.noise.impl 验证码噪点生成对象 默认为DefaultNoise
// kaptcha.noise.color 验证码噪点颜色 默认为Color.BLACK
// kaptcha.obscurificator.impl 验证码样式引擎 默认为WaterRipple
// kaptcha.word.impl 验证码文本字符渲染 默认为DefaultWordRenderer
// kaptcha.background.impl 验证码背景生成器 默认为DefaultBackground
// kaptcha.background.clear.from 验证码背景颜色渐进 默认为Color.LIGHT_GRAY
// kaptcha.background.clear.to 验证码背景颜色渐进 默认为Color.WHITE
// kaptcha.image.width 验证码图片宽度 默认为200
// kaptcha.image.height 验证码图片高度 默认为50

}

工具类

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
36
37
38
39
40
41
42
43
44
public class HTTPResponse {
private Integer status;
private String message;
private Object data;

public static HTTPResponse ok(String message) {
return new HTTPResponse(200, message);
}
public static HTTPResponse ok(Object data) {
return new HTTPResponse(200, data);
}
public static HTTPResponse ok(String message,Object data) {
return new HTTPResponse(200, message, data);
}
public static HTTPResponse error(Integer status, String message) {
return new HTTPResponse(500, message, null);
}
public static HTTPResponse error(String message) {
return new HTTPResponse(500, message, null);
}
public static HTTPResponse error(String message,Object data) {
return new HTTPResponse(500, message, data);
}
protected HTTPResponse() {super();}
private HTTPResponse(Integer status, String message) {
this.status = status;
this.message = message;
}
private HTTPResponse(Integer status, Object data) {
this.status = status;
this.data = data;
}
private HTTPResponse(Integer status, String message, Object data) {
this.status = status;
this.message = message;
this.data = data;
}
public Integer getStatus() {return status;}
public String getMessage() {return message;}
public Object getData() {return data;}
public void setStatus(Integer status) {this.status = status;}
public void setMessage(String message) {this.message = message;}
public void setObject(Object data) {this.data = data;}
}
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import java.net.InetAddress;
import java.net.UnknownHostException;

import javax.servlet.http.HttpServletRequest;

import org.springframework.security.core.context.SecurityContextHolder;

import xyz.nieqiang.webapp.entity.Admin;
public class UserIPUtil {

//获取当前用户的用户名的ID
public static Integer getCurrentAdminId() {
return ((Admin)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId();
}

//获取当前用户的用户姓名
public static String getCurrentAdminName() {
return ((Admin)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getName();
}

//获取当前用户的登录名
public static String getCurrentAdminUserName() {
return ((Admin)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername();
}

//获取IP
public static String getIPAddress(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "未获取到IP";
}
return ipAddress;
}
}

登录拦截器

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.code.kaptcha.Constants;

import xyz.nieqiang.webapp.util.HTTPResponse;
/**
* 验证码拦截器
* @CreationDate:2020年7月16日下午4:27:07
* @Author:NieQiang
* @ComputerName:Administrator
*/
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private static final Logger logger = LoggerFactory.getLogger(LoginFilter.class);

@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
if("POST".equalsIgnoreCase(request.getMethod()) && "/login".equals(request.getServletPath())) {
String code = request.getParameter("code");
String kaptchaCode = (String) request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
logger.info("用户输入验证码:{}========session中存的验证码:{}",code,kaptchaCode);
if(StringUtils.isEmpty(code)) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(401);
PrintWriter printWriter = response.getWriter();
HTTPResponse httpResponse = HTTPResponse.error("验证码不能为空!","");
printWriter.write(new ObjectMapper().writeValueAsString(httpResponse));
printWriter.flush();
printWriter.close();
return;
}else if(!kaptchaCode.toLowerCase().equals(code.toLowerCase())) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(401);
PrintWriter printWriter = response.getWriter();
HTTPResponse httpResponse = HTTPResponse.error("验证码错误!","");
printWriter.write(new ObjectMapper().writeValueAsString(httpResponse));
printWriter.flush();
printWriter.close();
return;
}
}
chain.doFilter(request, response);
}
}

验证码Controller

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
36
37
38
39
40
import java.awt.image.BufferedImage;
import java.io.IOException;

import javax.annotation.security.PermitAll;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.google.code.kaptcha.Constants;
import com.google.code.kaptcha.Producer;

@RestController
public class KaptchaController {
@Autowired
private Producer producer;

@PermitAll//注解放行
@GetMapping("/captcha.jpg")
public void captcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");

// 生成文字验证码
String text = producer.createText();
// 生成图片验证码
BufferedImage image = producer.createImage(text);
// 保存到验证码到 session
request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, text);

ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
IOUtils.closeQuietly(out);
}
}

SecurityConfiguration配置

SecurityConfiguration

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.session.InvalidSessionStrategy;
import com.fasterxml.jackson.databind.ObjectMapper;
import cn.hutool.http.HttpStatus;
import xyz.nieqiang.webapp.entity.Admin;
import xyz.nieqiang.webapp.service.impl.AdminServiceImpl;
import xyz.nieqiang.webapp.util.HTTPResponse;
import xyz.nieqiang.webapp.util.UserIPUtil;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);

@Autowired
private AdminServiceImpl adminServiceImpl;

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

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(adminServiceImpl);
}

@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/captcha.jpg");//不通过security过滤连放行
}

@Override
protected void configure(HttpSecurity http) throws Exception {
//这里是在校验密码之前先校验验证码,如果验证码错误就不检验用户名和密码了
http.addFilterBefore(new LoginFilter(), UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests()
.antMatchers("/captcha.jpg/**").permitAll()// 验证码//通过security过滤连放行
.anyRequest().authenticated()//其他访问必须登录
.and().formLogin()
.successHandler(new AuthenticationSuccessHandler() {

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
logger.info("{}=={}=={}",UserIPUtil.getCurrentAdminName(), new Date(), UserIPUtil.getIPAddress(request));
response.setContentType("application/json;charset=UTF-8");
PrintWriter printWriter = response.getWriter();
Admin admin = (Admin) authentication.getPrincipal();
admin.setPassword(null);//不返回password字段

HTTPResponse httpResponse = HTTPResponse.ok("登录成功!",admin);
String s = new ObjectMapper().writeValueAsString(httpResponse);
printWriter.write(s);
printWriter.flush();
printWriter.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
logger.info("登录失败的日志");
response.setContentType("application/json;charset=UTF-8");
PrintWriter printWriter = response.getWriter();
HTTPResponse httpResponse = HTTPResponse.error("登录失败", "");
if(exception instanceof LockedException) {
httpResponse.setMessage("账户被锁定!请联系管理员");
}else if(exception instanceof AccountExpiredException) {
httpResponse.setMessage("账户已过期!请联系管理员");
}else if(exception instanceof DisabledException) {
httpResponse.setMessage("账户被禁用!请联系管理员");
}else if(exception instanceof CredentialsExpiredException) {
httpResponse.setMessage("密码已过期!请联系管理员");
}else if(exception instanceof BadCredentialsException) {
httpResponse.setMessage("用户名或密码有误!请重新输入");
}
printWriter.write(new ObjectMapper().writeValueAsString(httpResponse));
printWriter.flush();
printWriter.close();
}
})
.permitAll()
.and()
.logout().logoutSuccessHandler(new LogoutSuccessHandler() {

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
logger.info("{}退出成功!",new Date());
response.setContentType("application/json;charset=UTF-8");
PrintWriter printWriter = response.getWriter();

HTTPResponse httpResponse = HTTPResponse.ok("退出成功!",null);
String s = new ObjectMapper().writeValueAsString(httpResponse);
printWriter.write(s);
printWriter.flush();
printWriter.close();
}
}).permitAll()
.and()
.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(401);
PrintWriter printWriter = response.getWriter();
HTTPResponse httpResponse = HTTPResponse.error("访问失败!","");
if(authException instanceof InsufficientAuthenticationException) {
httpResponse.setMessage("尚未登录或者登录身份已过期,请登录或者重新登录!");
}
printWriter.write(new ObjectMapper().writeValueAsString(httpResponse));
printWriter.flush();
printWriter.close();
}
})
.and().csrf().disable()
.cors().disable();

http.sessionManagement().invalidSessionStrategy(new InvalidSessionStrategy() {

@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
logger.info("{}退出成功!",new Date());
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
PrintWriter printWriter = response.getWriter();

HTTPResponse httpResponse = HTTPResponse.error("身份过期!",null);
String s = new ObjectMapper().writeValueAsString(httpResponse);
printWriter.write(s);
printWriter.flush();
printWriter.close();
}
});
}
}

至此后端接口编写完毕

=====================以下是前端====================

前端重难点就是点击验证码刷新的现实,没啥好说的,直接贴代码吧

package.json依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"dependencies": {
"ant-design-vue": "^1.6.2",
"axios": "^0.19.2",
"font-awesome": "^4.7.0",
"bootstrap": "^4.5.0",
"core-js": "^3.6.5",
"element-ui": "^2.13.2",
"vue": "^2.6.11",
"vue-router": "^3.2.0",
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.4.0",
"@vue/cli-plugin-eslint": "~4.4.0",
"@vue/cli-plugin-router": "~4.4.0",
"@vue/cli-plugin-vuex": "~4.4.0",
"@vue/cli-service": "~4.4.0",
"babel-eslint": "^10.1.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"compression-webpack-plugin": "^4.0.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.11"
}

Axios封装

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import axios from 'axios'
import {Message} from 'element-ui'
import router from '../router'

axios.defaults.withCredentials = true; // 表示跨域请求时是否需要使用凭证,跨域访问需要发送cookie时一定要加
axios.interceptors.request.use(config=>{
return config;
}, err=> {
Message.error({message: '请求超时!'+err,showClose: true});
})


axios.interceptors.response.use(success=>{
if (success.status && success.status == 200 && success.data.status == 500) {
Message.error({
message: success.data.message+' '+success.data.data,
showClose: true
});
return;
}
if (success.data.message) {
Message.success({
message: success.data.message,
showClose: true
});
}
return success.data;
},err=> {
console.log(err,err.response,err.response.status);
if(err.response.status == 500){
// Message.error({message: "服务器连接失败⊙﹏⊙∥", showClose: true});
router.push('/500');
}else if (err.response.status == 504) {
Message.error({message: '服务器连接失败⊙﹏⊙∥'+err.response.data.message,showClose: true});
} else if (err.response.status == 404) {
Message.error({message: '资源不存在!',showClose: true});
} else if (err.response.status == 403) {
Message.error({message: '权限不足,请联系管理员!',showClose: true});
} else if (err.response.status == 401) {
Message.error({message:err.response.data.message,showClose: true});
router.replace('/');
sessionStorage.clear();
}else if (err.response.status == 500) {
Message.error({message:err.response.data.message,showClose: true});
}else if (err.response == '') {
this.$alert(err.response.message, {
confirmButtonText: '确定',
iconClass:'fa fa-exclamation-triangle',
title:'EXCEPTION!服务端存在异常!'
});
router.replace('/');
}else {
if (err.response.data.message) {
Message.error({message: err.response.data.message,showClose: true});
}else{
Message.error({message: '未知错误!',showClose: true});
}
}
}
)


let base = '';
export const postKeyValueRequest = (url, params)=> {
return axios({
method: 'POST',
url: `${base}${url}`,
data: params,
transformRequest: [function (data) {
let ret = ''
for (let it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret;
}],
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}


export const postRequest = (url, params)=>{
return axios({
method: 'POST',
url: `${base}${url}`,
data: params,
transformRequest: [function (data) {
let ret = ''
for (let it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
}],
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}

export const putRequest = (url, params) => {
return axios({
method: 'PUT',
url: `${base}${url}`,
data: params,
transformRequest: [function (data) {
let ret = ''
for (let it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
}],
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}

export const getRequest = (url) => {
return axios({
method: 'GET',
url: `${base}${url}`
});
}

export const deleteRequest = (url) => {
return axios({
method: 'DELETE',
url: `${base}${url}`
});
}

export const uploadFileRequest = (url,params) => {
return axios({
method: 'POST',
url: `${base}${url}`,
data: params,
headers: {
'Content-Type': 'multipart/form-data'
}
});
}

export const downloadRequest = (url, params)=>{
return axios({
method: 'GET',
url: `${base}${url}`,
data: params,
responseType: 'blob',
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8'
}
});
}

main.js

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import 'ant-design-vue/dist/antd.css';
import 'element-ui/lib/theme-chalk/index.css';
import 'font-awesome/css/font-awesome.min.css'

/**Ant Design */
import {
Layout,Menu,Tabs,Button,Icon,Input,Breadcrumb,
Result,FormModel,Form ,Upload,Card,Modal
} from 'ant-design-vue';
Vue.use(Layout);
Vue.use(Menu);
Vue.use(Tabs);
Vue.use(Button);
Vue.use(Icon);
Vue.use(Input);
Vue.use(Breadcrumb);
Vue.use(Result);
Vue.use(Upload);
Vue.use(Card);
Vue.use(Modal);
Vue.use(Form);
Vue.use(FormModel);

/**ElementUIBloodLitchi*/
import {Image,Checkbox,Tooltip,Message,MessageBox,Popover,
Loading, Notification} from 'element-ui';
Vue.use(Image);
Vue.use(Checkbox);
Vue.use(Tooltip);
Vue.use(Popover);
Vue.use(Loading);
Vue.prototype.$message = Message;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$notify = Notification;

import {
postKeyValueRequest,postRequest,
putRequest,getRequest,deleteRequest,
uploadFileRequest,downloadRequest
}from "@/util/axiosAPI";
// 方法封装
Vue.prototype.postKeyValueRequest = postKeyValueRequest;
Vue.prototype.postRequest = postRequest;
Vue.prototype.putRequest = putRequest;
Vue.prototype.getRequest = getRequest;
Vue.prototype.deleteRequest = deleteRequest;
Vue.prototype.uploadFileRequest = uploadFileRequest;
Vue.prototype.downloadRequest = downloadRequest;


Vue.config.productionTip = false

router.beforeEach((to, from,next)=>{
if(to.path=='/'){
next();
}else if(sessionStorage.getItem('user') != null && sessionStorage.getItem('user')){
next();
}else if(sessionStorage.getItem('user')==null){
next({path: '/'});
}else{
next({path: '/'});
}
});

new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')

Login.vue

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<template>
<div class="login">
<a-card title="登 录" :bordered="true" class="containers">
<a-form-model layout="vertical" :model="loginForm" @submit="handleSubmit" @submit.native.prevent>
<a-form-model-item>
<a-input v-model="loginForm.username" placeholder="用户名" style="width:300px" allowClear>
<a-icon slot="prefix" type="user" style="color:rgba(0,0,0,.25)"/>
</a-input>
</a-form-model-item>
<a-form-model-item>
<a-input-password v-model="loginForm.password" placeholder="密 码" style="width:300px" allowClear>
<a-icon slot="prefix" type="lock" style="color:rgba(0,0,0,.25)" />
</a-input-password>
</a-form-model-item>
<a-form-model-item>
<a-input placeholder="点击图片刷新验证码" style="width:170px" v-model="loginForm.code" allowClear></a-input>
<img :src="verCode" class="verCode" @click="newVerCode">
</a-form-model-item>
<a-form-model-item>
<a-button type="primary" html-type="submit" style="width:300px"
:disabled="loginForm.username === '' || loginForm.password === '' || loginForm.code == ''">
登 录
</a-button>
</a-form-model-item>
</a-form-model>
</a-card>
</div>
</template>
<script>
export default {
data() {
return {
loginForm:{
username: '',
password: '',
code:""
},
verCode:""
};
},
methods: {
handleSubmit() {
this.postRequest('/docs/login',
this.loginForm
).then(resp=>{
console.log(resp);
if(resp){
sessionStorage.clear();
sessionStorage.setItem('user',JSON.stringify(resp.data));
this.$router.replace('/all')
}
});
},
newVerCode(){//刷新验证码后面加上随机数防止缓存导致刷新验证码失败
this.verCode = "/docs/captcha.jpg?m="+Math.random();
}
},
mounted(){
this.newVerCode();
}
};
</script>
<style scoped>
.login{
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.containers{
width: 400px;
margin: auto;
}

.verCode{
width:100px;
height:31px;
object-fit: fill;
margin-left: 30px;
}
</style>

评论