文章目录
1 项目搭建
1.1 新建项目
Next
1.2 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>testspringboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>testspringboot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
总有一些莫名其妙的配置问题,直接替换该pom,省事!
1.3 application.yml
采用的数据库连接池是默认的Hikari。
将application.properties改名为application.yml,添加配置项:
server:
port: 8090
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/authority?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
## 最小空闲连接数量
minimum-idle: 5
## 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
## 连接池最大连接数,默认是10
maximum-pool-size: 500
## 此属性控制从池返回的连接的默认自动提交行为,默认值:true
auto-commit: true
## 连接池母子
pool-name: MyHikariCP
## 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
max-lifetime: 1800000
## 数据库连接超时时间,默认30秒,即30000
connection-timeout: 30000
connection-test-query: SELECT 1
servlet: #修改前端上传文件大小配置
multipart:
max-file-size: 500MB
max-request-size: 1024MB
mvc: #访问静态资源
static-path-pattern: /static/**
web:
resources:
static-locations: classpath:/static,classpath:/public,classpath:/resources,classpath:/META-INF/resources
mybatis: #resources下新建的放置mapper文件的文件夹应与此处同名
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.lzh.authoritysystem.model
1.4 index.html
在resources
下新建templates
文件夹,创建index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" type="text/css" href="../static/css/mycss.css">
</head>
<body>
<h1>Authority System Test</h1>
<div></div>
<img src="../static/img/login/bg.png">
<script src="../static/js/myjs.js"></script>
<script>
console.log(name)
</script>
</body>
</html>
4 配置Swagger
- 添加依赖
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
- 添加配置文件
SwaggerConfig
package com.lzh.authoritysystem.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Configuration
@EnableSwagger2
public class SwaggerConfig implements WebMvcConfigurer{
@Bean
public Docket controllerApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("AuthoritySystem Swagger")
.description("众里寻她千百度,蓦然回首,那人却在灯火阑珊处。")
.contact(new Contact("lzh", null, null))
.version("版本号:1.0")
.build())
.groupName("test") //配置分组
// .globalOperationParameters(globalOperationParameters()) //配置文档参数
.enable(true) //enable是否启动Swagger,如果为False,则Swagger不能再浏览器中访问
.select()
.apis(RequestHandlerSelectors.basePackage("com.lzh.authoritysystem.controller")) //扫描接口
.paths(PathSelectors.any())
.build()
//下面两行为header中的Authorization认证,如不需要可不添加
// ApiKey的name需与SecurityReference的reference保持一致
.securityContexts(securityContext())
.securitySchemes(securitySchemes());
}
/**
* 设置 token 的两种方式
* 方式一:接口单独传token,只需设置globalOperationParameters,
* 方式二:设置全局token,需要设置securitySchemes和securityContexts.
*
* @return
*/
private List<Parameter> globalOperationParameters(){
ParameterBuilder paramBuilder = new ParameterBuilder()
.name("token")
.description("token令牌")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false); // false表示header中的token参数可以为空
List<Parameter> params = new ArrayList<>();// 创建参数集合
params.add(paramBuilder.build());
return params;
}
private List<ApiKey> securitySchemes() {
//设置请求头信息
List<ApiKey> result = new ArrayList<>();
ApiKey apiKey = new ApiKey("token", "token", "header");
result.add(apiKey);
return result;
}
private List<SecurityContext> securityContext() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
List<SecurityReference> securityReferenceList = new ArrayList<>();
securityReferenceList.add(new SecurityReference("token", authorizationScopes));
SecurityContext securityContext = SecurityContext.builder()
.securityReferences(securityReferenceList)
//.forPaths(PathSelectors.regex("/*.*"))
.build();
List<SecurityContext> securityContextList = new ArrayList<>();
securityContextList.add(securityContext);
return securityContextList;
}
/**
* @Author: lequal
* @Description 添加放行资源,解决无法访问 swagger-ui.html
* @Date 2022/7/28 0028 10:28
* @Param [registry]
* @return void
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
特别的:为方法增加token
@RestController
@RequestMapping("header")
@Api(tags = "为单个接口添加header")
public class HeaderController {
@GetMapping("token")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "header", name = "token", required = true),
})
@ApiOperation("token")
public R<Void> header() {
return R.ok();
}
}
这种方法的缺点就是需要手动在每个方法上添加@ApiImplicitParam注解来指定header参数
- 常用注解
- Api:写在Controller接口类上,对该Controller做整体说明
- ApiOperation:写在Controller中的方法(某一具体接口)上,对该接口做说明
- ApiParam:写在Controller中方法的参数上,对该参数做说明
@Api(tags = "controller注释")
@RestController
public class TestController {
@ApiOperation("方法注释")
@GetMapping("/testGet")
public Map testGet(@RequestParam @ApiParam("参数注释") String name, @RequestParam List list){
Map m = new HashMap();
m.put("name", name);
return m;
}
}
- 访问文档
在本地主机启动项目后,访问以下地址即可查看:http://localhost:8090/swagger-ui.html
5 工具类—封装后台接口通用返回
统一的封装,让代码看起来更加高级,避免每个接口返回乱糟糟,不统一。
- 通用返回工具类
package com.lzh.authoritysystem.utils;
import java.io.Serializable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
public class ResponseResult<T> extends LinkedHashMap<String, Object> implements Serializable {
private static final long serialVersionUID = 1L;
private static final int CODE_SUCCESS = 200;
private static final int CODE_ERROR = 500;
private static final String CODE_STR = "code";
private static final String MSG_STR = "msg";
private static final String DATA_STR = "data";
private static final String SUCCESS_MSG_STR = "操作成功";
private static final String ERROR_MSG_STR = "操作失败";
private T data;
public ResponseResult() {
}
public ResponseResult(Object data, int code, String msg) {
this.setCode(code);
this.setMsg(msg);
this.setData(data);
}
public ResponseResult(int code, String msg, T data) {
this.setCode(code);
this.setMsg(msg);
this.setData(data);
}
public ResponseResult(Map<String, ?> map) {
this.setMap(map);
}
public Integer getCode() {
return (Integer)this.get(CODE_STR);
}
public String getMsg() {
return (String)this.get(MSG_STR);
}
public Object getData() {
return this.get(DATA_STR);
}
public ResponseResult<?> setCode(int code) {
this.put(CODE_STR, code);
return this;
}
public ResponseResult<?> setMsg(String msg) {
this.put(MSG_STR, msg);
return this;
}
public ResponseResult<?> setData(Object data) {
this.put(DATA_STR, data);
return this;
}
public ResponseResult<?> set(String key, Object data) {
this.put(key, data);
return this;
}
public ResponseResult<?> setMap(Map<String, ?> map) {
Iterator var2 = map.keySet().iterator();
while(var2.hasNext()) {
String key = (String)var2.next();
this.put(key, map.get(key));
}
return this;
}
public static ResponseResult<?> success() {
return new ResponseResult<>(CODE_SUCCESS, SUCCESS_MSG_STR, (Object)null);
}
public static ResponseResult<String> success(String msg) {
return new ResponseResult<>(CODE_SUCCESS, SUCCESS_MSG_STR, msg);
}
public static <T> ResponseResult<T> success(T obj) {
return new ResponseResult<>(CODE_SUCCESS, SUCCESS_MSG_STR, obj);
}
public static <T> ResponseResult<T> error() {
return new ResponseResult(CODE_ERROR, ERROR_MSG_STR, (Object)null);
}
public static ResponseResult<String> error(String msg) {
return new ResponseResult<>(CODE_ERROR, ERROR_MSG_STR, msg);
}
}
- 全局异常捕获工具类
异常不需要自己在controller里判断返回,写个全局异常捕获,如果代码执行过程发生异常,就能自己按照我们这个格式抛出。
package com.lzh.authoritysystem.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandle {
/**
* 系统异常
*/
@ExceptionHandler(Exception.class)
public ResponseResult<String> handleException(Exception e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'"+ requestURI +"',发生系统异常: " + e.getMessage());
return ResponseResult.error("'" + requestURI + "' --- " + e.getMessage());
}
}
- 使用方式
ResponseResult.success(o);
ResponseResult.error(“'” + requestURI + "’ — " + e.getMessage());
@RestController
public class TestController {
@ApiOperation("方法注释")
@GetMapping("/testGet")
public ResponseResult testGet(@RequestParam @ApiParam("参数注释") String name, @RequestParam List list){
if(name.equals("123")){
Map m = new HashMap();
m.put("name", name);
return ResponseResult.success(m);
}
int a = 1 / 0;
return null;
}
}
6 用户认证与授权
6.1 Spring Security介绍
一般Web应用的需要进行认证和授权。
- 认证(Authentication):验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
- 授权(Authorization):经过认证后判断当前用户是否有权限进行某个操作
而认证和授权就是
Spring Security
作为安全框架的核心功能。
Spring Security
登录认证主要涉及两个重要的接口UserDetailService
和UserDetails
接口。UserDetailService
接口主要定义了一个方法loadUserByUsername(String username)
用于完成用户信息的查询,其中username
就是登录时的登录名称,登录认证时,需要自定义一个实现类实现UserDetailService
接口,完成数据库查询,该接口返回UserDetail
。
UserDetail
主要用于封装认证成功时的用户信息,即UserDetailService
返回的用户信息,可以用Spring
自己的User
对象,但是最好是实现UserDetail
接口,自定义用户对象。
6.2 Spring Security认证步骤
- 自定
UserDetails
类:当实体对象字段不满足时需要自定义UserDetails
,一般都要自定义UserDetails
。- 自定义
UserDetailsService
类,主要用于从数据库查询用户信息。- 创建登录认证成功处理器,认证成功后需要返回
JSON
数据,菜单权限等。- 创建登录认证失败处理器,认证失败需要返回
JSON
数据,给前端判断。- 创建匿名用户访问无权限资源时处理器,匿名用户访问时,需要提示
JSON
。- 创建认证过的用户访问无权限资源时的处理器,无权限访问时,需要提示
JSON
。- 配置
Spring Security
配置类,把上面自定义的处理器交给Spring Security
。
6.3 Spring Security认证实现
6.3.1 添加依赖
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--token-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
6.3.2 自定义UserDetails实现类
当实体对象字段不满足时
Spring Security
认证时,需要自定义UserDetails
。
- 将User类实现UserDetails接口。(本文用继承类LoginUser实现该接口)
- 将原有的isAccountNonExpired、isAccountNonLocked、isCredentialsNonExpired和isEnabled属性修改成boolean类型,同时添加authorities属性。
注意: 上述4个属性只能是非包装类的boolean类型属性,且默认值设置为true。
package com.lzh.authoritysystem.dto;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import com.lzh.authoritysystem.model.Permission;
import com.lzh.authoritysystem.model.SysUser;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Data
public class LoginUser extends SysUser implements UserDetails {
private static final long serialVersionUID = -1379274258881257107L;
private List<Permission> permissions;
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
return permissions.parallelStream().filter(p -> !StringUtils.isEmpty(p.getPermission()))
.map(p -> new SimpleGrantedAuthority(p.getPermission())).collect(Collectors.toSet());
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
// do nothing
}
// 账户是否未过期
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
// 账户是否未锁定
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return getStatus() != Status.LOCKED;
}
// 密码是否未过期
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 账户是否激活
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
6.3.3 编写UserService接口、实现类
和正常mybatis
使用套路一样,首先要提前写好mapper
接口和xml
文件
UserService接口:
package com.lzh.authoritysystem.service;
import com.lzh.authoritysystem.model.SysUser;
public interface UserService {
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
SysUser getUser(String username);
}
UserService接口实现类:
package com.lzh.authoritysystem.service.impl;
import java.util.List;
import com.lzh.authoritysystem.dao.UserDao;
import com.lzh.authoritysystem.model.SysUser;
import com.lzh.authoritysystem.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
@Override
public SysUser getUser(String username) {
return userDao.getUser(username);
}
}
6.3.4 自定义UserDetailsService接口实现类
package com.lzh.authoritysystem.service.impl;
import java.util.List;
import com.lzh.authoritysystem.dao.PermissionDao;
import com.lzh.authoritysystem.dto.LoginUser;
import com.lzh.authoritysystem.model.Permission;
import com.lzh.authoritysystem.model.SysUser;
import com.lzh.authoritysystem.service.UserService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* spring security登陆处理<br>
* <p>
* 密码校验请看文档(02 框架及配置),第三章第4节
*
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionDao permissionDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userService.getUser(username);
if (sysUser == null) {
throw new AuthenticationCredentialsNotFoundException("用户名不存在");
} else if (sysUser.getStatus() == SysUser.Status.LOCKED) {
throw new LockedException("用户被锁定,请联系管理员");
} else if (sysUser.getStatus() == SysUser.Status.DISABLED) {
throw new DisabledException("用户已作废");
}
LoginUser loginUser = new LoginUser();
BeanUtils.copyProperties(sysUser, loginUser);
// 获取用户权限
List<Permission> permissions = permissionDao.listByUserId(sysUser.getId());
loginUser.setPermissions(permissions);
return loginUser;
}
}
6.3.5 编写自定义理器
package com.lzh.authoritysystem.config;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.lzh.authoritysystem.dto.LoginUser;
import com.lzh.authoritysystem.utils.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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.logout.LogoutSuccessHandler;
/**
* spring security处理器
*
*/
@Configuration
public class SecurityHandlerConfig {
/**
* 登陆认证成功处理器
*
* @return
*/
@Bean
public AuthenticationSuccessHandler loginSuccessHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//设置响应的编码格式
response.setContentType("application/json;charset=utf-8");
//获取当前登录用户信息
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//将对象转为JSON格式,并消除循环引用
String result = JSON.toJSONString(ResponseResult.success(loginUser), SerializerFeature.DisableCircularReferenceDetect);
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
};
}
/**
* 登陆认证失败处理器
*
* @return
*/
@Bean
public AuthenticationFailureHandler loginFailureHandler() {
return new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
//设置响应的编码格式
response.setContentType("application/json;charset=utf-8");
//判断异常类型
String msg = null;
if(exception instanceof BadCredentialsException) {
msg = "密码错误,登录失败!";
}else if(exception instanceof AccountExpiredException){
msg = "账户过期,登录失败!";
}else if(exception instanceof CredentialsExpiredException){
msg = "密码过期,登录失败!";
}else if(exception instanceof DisabledException){
msg = "账户被禁用,登录失败!";
}else if(exception instanceof LockedException){
msg = "账户被锁,登录失败!";
}else if(exception instanceof InternalAuthenticationServiceException){
msg = "账户不存在,登录失败!";
}else {
msg = exception.getMessage();
}
//将对象转为JSON格式
String result = JSON.toJSONString(ResponseResult.error(msg));
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
};
}
/**
* 未登录,匿名无权限访问
*
* @return
*/
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
//设置响应的编码格式
response.setContentType("application/json;charset=utf-8");
//将对象转为JSON格式,并消除循环引用
String result = JSON.toJSONString(ResponseResult.error("请先登录"));
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
};
}
/**
* 退出处理器
*
* @return
*/
@Bean
public LogoutSuccessHandler logoutSussHandler() {
return new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//设置响应的编码格式
response.setContentType("application/json;charset=utf-8");
//将对象转为JSON格式,并消除循环引用
String result = JSON.toJSONString(ResponseResult.success( "退出成功"));
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
};
}
}
6.3.6 编写Spring Security配置类
package com.lzh.authoritysystem.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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;
/**
* spring security配置
*
*/
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//登陆成功
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
//登陆失败
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private UserDetailsService userDetailsService;
/**
* 注入加密类
* @return
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 处理登录认证
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登陆处理过程
http.formLogin() //表单登录
.loginProcessingUrl("/login") //登录请求的url地址
.successHandler(authenticationSuccessHandler) //认证成功处理器
.failureHandler(authenticationFailureHandler) //认证失败处理器
.usernameParameter("username") //前端输入框name属性,默认即为username
.passwordParameter("password") //前端输入框name属性,默认即为password
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不创建session
.and()
.authorizeRequests() //设置需要拦截的请求
.antMatchers("/", "/*.html", "/favicon.ico", "/css/**", "/js/**", "/fonts/**", "/layui/**", "/img/**",
"/v2/api-docs/**", "/swagger-resources/**", "/webjars/**", "/pages/**", "/druid/**",
"/static/**").permitAll() //登录请求方向(不拦截)
.anyRequest().authenticated() //其他一律请求都要进行身份认证
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint) //匿名无权限访问
.and()
.cors(); //支持跨域
http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 解决不允许显示在iframe的问题
http.headers().frameOptions().disable();
http.headers().cacheControl();
}
/**
* 配置认证处理器
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
6.3.7 替换数据库中用户密码
因为数据库中密码为加密存储,因此需要自定义生成密码替换库中密码
@SpringBootTest
class AuthoritySystemApplicationTests {
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
@Test
void testConnection() throws Exception {
System.out.println(bCryptPasswordEncoder.encode("123456"));
}
}
IDEA控制台即可生成加密后的密码,根据加密规则,同一密码多次加密后的结果各不相同。
6.3.8 测试登录认证接口
6.4 认证成功后生成并返回token
Token
是服务器端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登陆后,服务器生成一个token
并将其返回给客户端,客户端每次请求数据需携带token
。
6.4.1 在application.yml中新增配置
token:
expireSeconds: 60 #token过期秒数
jwtSecret: "(XIAO:)_$^11244^%$_(WEI:)_@@++--(LAO:)_++++_.sds_(SHI:)" #私钥
6.4.2 定义返回接口类LoginResult
package com.lzh.authoritysystem.model;
import com.lzh.authoritysystem.dto.LoginUser;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Date;
@Data
@AllArgsConstructor
public class LoginResult{
private static final long serialVersionUID = 4566334160572911795L;
/**
* LoginUser,登陆成功返回的UserDeatisls
*/
private LoginUser loginUser;
/**
* token
*/
private String token;
/**
* 过期时间
*/
private Date expireTime;
}
6.4.3 工具类JwtUtils
package com.lzh.authoritysystem.utils;
import com.lzh.authoritysystem.dto.LoginUser;
import com.lzh.authoritysystem.model.SysUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
@Data
public class JwtUtils {
@Value("${token.expireSeconds}")
private Integer expireSeconds;
@Value("${token.jwtSecret}")
private String jwtSecret;
/**
* 从数据声明生成令牌
*
* @param claims
* @return
*/
private String generateToken(Map<String, Object> claims){
Date expriationDate = new Date(System.currentTimeMillis() + expireSeconds * 1000);
return Jwts.builder()
.setClaims(claims)
.setExpiration(expriationDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
/**
* 从令牌中获取数据声明
*
* @param token
* @return
*/
public Claims getClaimsFromToken(String token){
Claims claims;
try {
claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
}catch (Exception e){
claims = null;
}
return claims;
}
/**
* 通过用户名生成令牌
*
* @param UserDetails接口实现类 loginUser
* @return
*/
public String generateToken(LoginUser loginUser){
Map<String, Object> claims = new HashMap<>(2);
claims.put(Claims.SUBJECT, loginUser.getUsername());
claims.put(Claims.ISSUED_AT, new Date());
return generateToken(claims);
}
/**
* 从令牌中获取用户名
*
* @param token
* @return
*/
public String getUsernameFromToken(String token){
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
}catch (Exception e){
username = null;
}
return username;
}
/**
* 判断token是否过期
*
* @param token
* @return
*/
public Boolean isTokenExpired(String token){
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
}
/**
* 刷新token
*
* @param token
* @return
*/
public String refreshToken(String token){
String refreshToken;
try{
Claims claims = getClaimsFromToken(token);
claims.put(Claims.ISSUED_AT, new Date());
refreshToken = generateToken(claims);
}catch (Exception e){
refreshToken = null;
}
return refreshToken;
}
/**
* 验证token是否有效
*
* @param token
* @param UserDetails接口实现类 loginUser
* @return
*/
public Boolean validateToken(String token, LoginUser loginUser){
SysUser sysUser = (SysUser) loginUser;
String username = getUsernameFromToken(token);
try{
return (username.equals(sysUser.getUsername())) && !isTokenExpired(token);
}catch (Exception e){
return false;
}
}
}
6.4.4 在登陆认证成功处理器中加入返回类LoginResult
@Autowired
private JwtUtils jwtUtils;
/**
* 登陆认证成功处理器
*
* @return
*/
@Bean
public AuthenticationSuccessHandler loginSuccessHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//设置响应的编码格式
response.setContentType("application/json;charset=utf-8");
//获取当前登录用户信息
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//生成token
String token = jwtUtils.generateToken(loginUser);
//设置token的签名密钥及过期时间
long expireTime = Jwts.parser()
.setSigningKey(jwtUtils.getJwtSecret()) //设置签名密钥
.parseClaimsJws(token.replace("jwt_", ""))
.getBody().getExpiration().getTime(); //设置过期时间
LoginResult loginResult = new LoginResult(loginUser, token, new Date(expireTime));
//将对象转为JSON格式,并消除循环引用
String result = JSON.toJSONString(ResponseResult.success(loginResult), SerializerFeature.DisableCircularReferenceDetect);
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
};
}
6.4.5 测试
6.5 token认证
本案例使用数据库存储token
6.5.1 新增TokenModel
package com.lzh.authoritysystem.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Date;
@Data
@AllArgsConstructor
public class TokenModel {
private static final long serialVersionUID = 4566334160572911795L;
/**
* token
*/
private String token;
/**
* LoginUser的json串
*/
private String userinfo;
/**
* 过期时间
*/
private Date expireTime;
private Date createTime;
private Date updateTime;
}
6.5.2 新增TokenDao
package com.lzh.authoritysystem.dao;
import com.lzh.authoritysystem.model.TokenModel;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface TokenDao {
@Insert("insert into t_token(token, userinfo, expireTime, createTime, updateTime) values (#{token}, #{userinfo}, #{expireTime}, #{createTime}, #{updateTime})")
int save(TokenModel model);
@Select("select * from t_token t where t.token = #{token}")
TokenModel getByToken(String token);
@Select("select * from t_token t where t.userinfo = #{userinfo}")
TokenModel getByUserinfo(String userinfo);
@Update("update t_token t set t.token = #{token}, t.expireTime = #{expireTime}, t.updateTime = #{updateTime} where t.userinfo = #{userinfo}")
int updateByUserinfo(TokenModel model);
@Delete("delete from t_token where token = #{token}")
int delete(String token);
}
6.5.3 在登陆认证成功处理器中存储token
/**
* 登陆认证成功处理器
*
* @return
*/
@Bean
public AuthenticationSuccessHandler loginSuccessHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//设置响应的编码格式
response.setContentType("application/json;charset=utf-8");
//获取当前登录用户信息
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//生成token
String token = jwtUtils.generateToken(loginUser);
//设置token的签名密钥及过期时间
long expireTime = Jwts.parser()
.setSigningKey(jwtUtils.getJwtSecret()) //设置签名密钥
.parseClaimsJws(token.replace("jwt_", ""))
.getBody().getExpiration().getTime(); //设置过期时间
LoginResult loginResult = new LoginResult(loginUser, token, new Date(expireTime));
//token表在数据库中存储的用户信息
String userinfo = loginUser.getId() + "-" + loginUser.getUsername();
//判断数据库中是否有该用户登录的token
TokenModel tokenModel = new TokenModel(token, userinfo, new Date(expireTime),
new Date(), new Date());
TokenModel tokenModelInDB = tokenDao.getByUserinfo(userinfo);
if(tokenModelInDB != null){
//更新数据库中的token
tokenDao.updateByUserinfo(tokenModel);
}else{
//将token存储到数据库中
tokenDao.save(tokenModel);
}
//将对象转为JSON格式,并消除循环引用
String result = JSON.toJSONString(ResponseResult.success(loginResult), SerializerFeature.DisableCircularReferenceDetect);
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
};
}
6.5.4 token过滤器
package com.lzh.authoritysystem.utils;
import java.io.IOException;
import java.util.Date;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.alibaba.fastjson.JSON;
import com.lzh.authoritysystem.dao.TokenDao;
import com.lzh.authoritysystem.dto.LoginUser;
import com.lzh.authoritysystem.model.TokenModel;
import io.jsonwebtoken.Jwts;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Token过滤器
*
*/
@Component
public class TokenFilter extends OncePerRequestFilter {
public static final String TOKEN_KEY = "token";
@Autowired
private TokenDao tokenDao;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//根据请求参数或者header获取token
String token = getToken(request);
if (StringUtils.isNotBlank(token)) {
//根据token表获取数据库中存储的token
TokenModel tokenModel = tokenDao.getByToken(token);
if (tokenModel != null){
String username = tokenModel.getUserinfo().split("-")[1];
LoginUser loginUser = (LoginUser) userDetailsService.loadUserByUsername(username);
//判断token是否有效
if(jwtUtils.validateToken(token, loginUser)){
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser,
null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}else{
//删除无效token
tokenDao.delete(token);
}
}
}
filterChain.doFilter(request, response);
}
/**
* 根据参数或者header获取token
*
* @param request
* @return
*/
public static String getToken(HttpServletRequest request) {
String token = request.getParameter(TOKEN_KEY);
if (StringUtils.isBlank(token)) {
token = request.getHeader(TOKEN_KEY);
}
return token;
}
}
6.5.5 在SecurityConfig中配置token过滤器
@Autowired
private TokenFilter tokenFilter;
/**
* 处理登录认证
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登陆处理过程---省略,具体看上文
//配置token过滤器
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
}
可在postman中测试不传token或者token过期,将返回500.
6.6 配置登陆页面
6.6.1 新建login.html
在resources
下新建templates
文件夹,创建login.html
注意
- 登录操作的
Ajax请求url
要和Security
配置类中的loginProcessingUrl
保持一致location.href
跳转时要携带token
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>登录</title>
<link href="../static/css/login.css" type="text/css" rel="stylesheet">
</head>
<body>
<div class="login">
<div class="message">后台管理系统</div>
<div id="darkbannerwrap"></div>
<form id="login-form" method="post" onsubmit="return false;">
<input id="username" name="username" placeholder="用户名" type="text" autocomplete="off">
<hr class="hr15">
<input id="password" name="password" placeholder="密码" type="password" autocomplete="off">
<hr class="hr15">
<button style="width: 100%;" type="submit" onclick="login(this)">登录</button>
<hr class="hr20">
<span id="info" style="color: red"></span>
</form>
</div>
</body>
<script src="../static/js/libs/jquery-2.1.1.min.js"></script>
<script type="text/javascript">
function login(obj) {
$(obj).attr("disabled", true);
var username = $.trim($('#username').val());
var password = $.trim($('#password').val());
if (username == "" || password == "") {
$("#info").html('用户名或者密码不能为空');
$(obj).attr("disabled", false);
} else {
$.ajax({
type : 'post',
url : '/login',
data : $("#login-form").serialize(),
success : function(data) {
console.log(data);
if(data.code == 200){
// localStorage.setItem("token", data.data.token);
location.href = '/goIndex?token=' + data.data.token;
}else{
console.log(data);
}
},
error : function(xhr, textStatus, errorThrown) {
var msg = xhr.responseText;
var response = JSON.parse(msg);
$("#info").html(response.message);
$(obj).attr("disabled", false);
}
});
}
}
</script>
</html>
6.6.2 新建页面导航配置页
package com.lzh.authoritysystem.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
*以下代码的作用,就是省略了写方法跳转页面
*/
@Configuration
public class IntercepterConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
registry.addViewController("/goLogin").setViewName("login");
registry.addViewController("/goIndex").setViewName("index");
}
}
前端知识点
1 html5实现电子签名
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>电子签字</title>
</head>
<body>
<canvas></canvas>
<div>
<button onclick="save()">保存</button>
<button onclick="cancel()">取消</button>
</div>
</body>
</html>
<script>
let canvas = document.querySelector('canvas')
let config = {
width: 500, // 宽度
height: 200, // 高度
lineWidth: 5, // 线宽
strokeStyle: 'red', // 线条颜色
lineCap: 'round', // 设置线条两端圆角
lineJoin: 'round', // 线条交汇处圆角
}
canvas.width = config.width
canvas.height = config.height
// 设置一个边框,方便我们查看及使用
canvas.style.border = '1px solid #000'
// 创建上下文
let ctx = canvas.getContext('2d')
// 设置填充背景色
ctx.fillStyle = 'transparent'
// 绘制填充矩形
ctx.fillRect(
0, // x 轴起始绘制位置
0, // y 轴起始绘制位置
config.width, // 宽度
config.height // 高度
);
// 保存上次绘制的 坐标及偏移量
let client = {
offsetX: 0, // 偏移量
offsetY: 0,
endX: 0, // 坐标
endY: 0
}
// 判断是否为移动端
let mobileStatus = (/Mobile|Android|iPhone/i.test(navigator.userAgent))
// 初始化
let inits = event => {
// 获取偏移量及坐标
let {
offsetX,
offsetY,
pageX,
pageY
} = mobileStatus ? event.changedTouches[0] : event
// 修改上次的偏移量及坐标
client.offsetX = offsetX
client.offsetY = offsetY
client.endX = pageX
client.endY = pageY
// 清除以上一次 beginPath 之后的所有路径,进行绘制
ctx.beginPath()
// 根据配置文件设置进行相应配置
ctx.lineWidth = config.lineWidth
ctx.strokeStyle = config.strokeStyle
ctx.lineCap = config.lineCap
ctx.lineJoin = config.lineJoin
// 设置画线起始点位
ctx.moveTo(client.endX, client.endY)
// 监听 鼠标移动或手势移动
window.addEventListener(mobileStatus ? "touchmove" : "mousemove", draw)
}
// 创建鼠标/手势按下监听器
window.addEventListener(mobileStatus ? "touchstart" : "mousedown", inits)
// 绘制
let draw = event => {
// 获取当前坐标点位
let {
pageX,
pageY
} = mobileStatus ? event.changedTouches[0] : event
// 修改最后一次绘制的坐标点
client.endX = pageX
client.endY = pageY
// 根据坐标点位移动添加线条
ctx.lineTo(pageX, pageY)
// 绘制
ctx.stroke()
}
// 结束绘制
let cloaseDraw = () => {
// 结束绘制
ctx.closePath()
// 移除鼠标移动或手势移动监听器
window.removeEventListener("mousemove", draw)
}
// 创建鼠标/手势 弹起/离开 监听器
window.addEventListener(mobileStatus ? "touchend" : "mouseup", cloaseDraw)
// 取消-清空画布
let cancel = () => {
// 清空当前画布上的所有绘制内容
ctx.clearRect(0, 0, config.width, config.height)
}
// 保存-将画布内容保存为图片
let save = () => {
// 将canvas上的内容转成blob流
canvas.toBlob(blob => {
console.log(blob);
// // 获取当前时间并转成字符串,用来当做文件名
let date = Date.now().toString()
// // 创建一个 a 标签
let a = document.createElement('a')
// // 设置 a 标签的下载文件名
a.download = `${date}.png`
// // 设置 a 标签的跳转路径为 文件流地址
a.href = URL.createObjectURL(blob)
// // 手动触发 a 标签的点击事件
a.click()
// // 移除 a 标签
a.remove()
})
}
</script>
保存
:下载图片