SpringBoot开发项目实战记录
零、 从0开始构建一个springboot项目
0.1 创建一个springboot项目,作为父工程
- 创建一个maven的项目
- 删除除了pom文件外的其他文件, 并配置pom文件如下面的
项目构建中的构建父项目pom
0.2 创建主项目工程,与逆向工程生成代码(用mybatis-plus的代码生成器)
1.创建模块 module
2. 根据下面的pom文件,配置主要项目的pom
3. 创建如下的文件,配置文件,启动类,并在下文中有效copy
0.3 构建逆向工程,也是创建module然后在mybatis-plus的代码生成器中根据数据库逆向代码
0.4 创建jwt工具类,完成对于配置,和公共返回配置。在下面的代码处有脑子的粘贴改代码
0.5 创建security和swagger的配置类等
一、项目构建
1.1 构建父项目 pom(注意不要楞头脑copy,要适配的改动)
<?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>
<modules>
<module>yeb_server</module>
<module>yeb_generator</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.jzq</groupId>
<artifactId>yeb</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>yeb</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<packaging>pom</packaging>
</project>
1.2 构建server项目结构
1. 创建配置文件 application.yml
server:
# 端口
port: 7777
spring:
# 数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/yeb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: Seven597
hikari:
# 连接池名
pool-name: DateHikariCP
# 最小空闲连接数
minimum-idle: 5
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
# 最大连接数 (10)
maximum-pool-size: 10
# 从连接池返回的连接自动提交
auto-commit: true
# 连接最大存活时间,0表示永久,默认为1800000(30分钟)
max-lifetime: 1800000
# 连接超时时间, 默认30000 (30秒)
connection-timeout: 30000
# 测试连接是否可用的查询语句
connection-test-query: 1
# Mybatis-plus配置
mybatis-plus:
# 配置Mapper映射文件
mapper-locations: classpath*:/mapper/*Mapper.xml
# 配置Mybatis数据返回类型别名(默认别名是类名)
type-aliases-package: com.jzq.server.pojo
configuration:
# 自动驼峰命名
map-underscore-to-camel-case: false
## Mybatis SQL 打印(方法接口所在的包,不是Mapper.xml所在的包)
logging:
level:
com.jzq.server.mapper: debug
2. 启动类 YebApplication
package com.jzq.server;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动类
*/
@SpringBootApplication
@MapperScan("com.jzq.server.mapper")
public class YebApplication {
public static void main(String[] args) {
SpringApplication.run(YebApplication.class, args);
}
}
3. 常用jar配置
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.11</maven.compiler.source>
<maven.compiler.target>1.11</maven.compiler.target>
<mysql.version>8.0.26</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<!-- google kaptcha依赖 -->
<!-- https://mvnrepository.com/artifact/com.github.penggle/kaptcha -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
<!--web 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok 依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--mysql 依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--mybatis-plus 依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<!-- swagger2 依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<!-- Swagger第三方ui依赖 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
<!--security 依赖-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.5</version>
</dependency>
<!--JWT 依赖-->
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
1.3 逆向工程——代码生成器(AutoGenerator)
1. 添加pom依赖
<!--web 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-plus 依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<!--mybatis-plus 代码生成器依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<!--freemarker 依赖-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<!--mysql 依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
2. 构建代码生成器类
package com.jzq.generator;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.FileOutConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.TemplateConfig;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* 执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
*
* @author zhoubin
* @since 1.0.0
*/
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/xxxxxxxxxxxx/src/main/java");
//作者
gc.setAuthor("seven");
//打开输出目录
gc.setOpen(false);
//xml开启 BaseResultMap
gc.setBaseResultMap(true);
//xml 开启BaseColumnList
gc.setBaseColumnList(true);
// 实体属性 Swagger2 注解
gc.setSwagger2(true);
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/yeb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia" +
"/Shanghai");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("xxx数据库用户名");
dsc.setPassword("xxx数据库密码");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.xxx.xxx(包名)")
.setEntity("pojo")
.setMapper("mapper")
.setService("service")
.setServiceImpl("service.impl")
.setController("controller");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/xxxxxx/src/main/resources/mapper/"
+ tableInfo.getEntityName() + "Mapper"
+ StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
//数据库表映射到实体的命名策略
strategy.setNaming(NamingStrategy.underline_to_camel);
//数据库表字段映射到实体的命名策略
strategy.setColumnNaming(NamingStrategy.no_change);
//lombok模型
strategy.setEntityLombokModel(true);
//生成 @RestController 控制器
strategy.setRestControllerStyle(true);
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
//表前缀
strategy.setTablePrefix("t_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
二、JWT 工具类的编写
2.1 配置pom与配置文件
1. pom准备
<!--security 依赖-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.5</version>
</dependency>
<!--JWT 依赖-->
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2. application.yml配置
# JWT
jwt:
# JWT存储的请求头
tokenHeader: Authorization
# JWT 解密加密使用的密钥
secret: yeb-secret
# JWT的超期限时间(30*60*24)
expiration: 604800
# JWT 负载中拿到开头
tokenHead: Bearer
2.2 工具类的编写
1. JWT工具类编写(JwtTokenUtil)
package com.jzq.server.config.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtTokenUtil工具类
*/
@Component
public class JwtTokenUtil {
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 根据用户信息生成token
* @param userDetails
* @return
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 根据荷载生成JWT Token
* @param claims
* @return
*/
public String generateToken(Map<String, Object> claims) {
System.out.println("secret:"+secret);
return Jwts.builder()
.setClaims(claims) // 设置载荷
.setExpiration(generateExpirationDate()) // 设置失效时间
.signWith(SignatureAlgorithm.HS512, secret) // 设置签名
.compact();
}
/**
* 生成token失效时间
* @return
*/
public Date generateExpirationDate() {
// 失效时间是就当前系统时间加 有效时间
return new Date(System.currentTimeMillis() + expiration*1000);
}
/**
* 从token中获取登录的用户名
* @param token
* @return
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getUserClaimFromToken(token);
username = claims.getSubject();
}catch (Exception e) {
e.printStackTrace();
username = null;
}
return username;
}
/**
* 从token中获取荷载
* @param token
* @return
*/
public Claims getUserClaimFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
/**
* 验证token是否有效 (token有效 + 用户一致)
* @param token
* @param userDetails
* @return
*/
public Boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token); // 终归是来源于token的
// 然后在token有效的前提下对比用户名是否一致
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token是否以及失效
* @param token
* @return
*/
public boolean isTokenExpired(String token) {
Date expireDate = getExpiredDateFromToken(token); // 拿到载荷里设置的有效时间
return expireDate.before(new Date()); // 验证载荷里的时间是否以及失效(失效返回true)
}
/**
* 获取token过期时间
* @param token
* @return
*/
public Date getExpiredDateFromToken(String token) {
Claims claims = getUserClaimFromToken(token);
return claims.getExpiration();
}
/**
* 判断token是否可以被刷新
* @param token
* @return
*/
public boolean canRefresh(String token) {
return !isTokenExpired(token);
}
/**
* 刷新token
* @param token
* @return
*/
public String refreshToken(String token) {
Claims claims = getUserClaimFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
2. 公共返回对象的封装
package com.jzq.server.config.result;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {
private long code;
private String message;
private Object result;
/**
* 成功返回 (提示)
* @param message
* @return
*/
public static RespBean success(String message) {
return new RespBean(200, message, null);
}
/**
* 成功返回 (结果)
* @param message
* @param result
* @return
*/
public static RespBean success(String message, Object result) {
return new RespBean(200, message, result);
}
/**
* 失败返回 (提示)
* @param message
* @return
*/
public static RespBean error(String message) {
return new RespBean(500, message, null);
}
}
三、正式开发
3.1 登录开发(通过security实现安全框架)
0. 让Admin用户类实现UserDetails接口(里面会实现一个角色的控制作用)(⭐这里必须在jopo实现这个接口,不然不能用SpringSecurity)
package com.jzq.server.pojo;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.jzq.server.util.Serializer.CustomAuthorityDeserializer;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/**
* <p>
*
* </p>
*
* @author seven
* @since 2022-01-02
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_admin")
@ApiModel(value="Admin对象", description="")
public class Admin implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "姓名")
private String name;
@ApiModelProperty(value = "手机号码")
private String phone;
@ApiModelProperty(value = "住宅电话")
private String telephone;
@ApiModelProperty(value = "联系地址")
private String address;
@ApiModelProperty(value = "是否启用")
@Getter(AccessLevel.NONE)
private Boolean enabled;
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "用户头像")
private String userFace;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "角色")
@TableField(exist = false)
private List<Role> roles;
@Override
@JsonDeserialize(using = CustomAuthorityDeserializer.class)
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
1. 创建登录实体类 AdminLoginParam
swagger文档注解:
@ApiModel : 注释在类上,用于描述
@ApiModelProperty: 注释在属性上, 用于描述和控制权限(是否必填)
package com.jzq.server.pojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户登录实体类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "AdminLogin对象", description = "") // swagger文档注解
public class AdminLoginParam {
@ApiModelProperty(value = "用户名", required = true)
private String username;
@ApiModelProperty(value = "密码", required = true)
private String password;
}
2. 登录的Controller层
swagger:
@Api(tags = “LoginController”) : 标注在类
@ApiOperation(value = “登录后返回token”): 标注在方法
package com.jzq.server.controller;
import com.jzq.server.config.result.RespBean;
import com.jzq.server.pojo.AdminLoginParam;
import com.jzq.server.service.IAdminService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 登录
*/
@Api(tags = "LoginController")
@RestController
public class LoginController {
@Autowired
private IAdminService adminService;
@ApiOperation(value = "登录后返回token")
@PostMapping("/login")
public RespBean login(AdminLoginParam adminLoginParam, HttpServletRequest request) {
return adminService.login(adminLoginParam.getUsername(), adminLoginParam.getPassword());
}
}
3. 登录的业务层Service
- 接口层
package com.jzq.server.service;
import com.jzq.server.config.result.RespBean;
import com.jzq.server.pojo.Admin;
import com.baomidou.mybatisplus.extension.service.IService;
import javax.servlet.http.HttpServletRequest;
/**
* <p>
* 服务类
* </p>
*
* @author seven
* @since 2022-01-02
*/
public interface IAdminService extends IService<Admin> {
/**
* 登录后返回token
* @param username
* @param password
* @param request
* @return
*/
RespBean login(String username, String password, HttpServletRequest request);
}
- 接口实现层
注意更新security登录用户对象
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());//第二个参数为口令(密码)不用传
通过SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken)设置
package com.jzq.server.service.impl;
import com.jzq.server.config.result.RespBean;
import com.jzq.server.config.security.JwtTokenUtil;
import com.jzq.server.pojo.Admin;
import com.jzq.server.mapper.AdminMapper;
import com.jzq.server.service.IAdminService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* 服务实现类
* </p>
*
* @author seven
* @since 2022-01-02
*/
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder; // 判断密码是否一致
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHead}") // 根据value注解拿到yml中设置的token头
private String tokenHead;
/**
* 登录成功返回token
* @param username
* @param password
* @param request
* @return
*/
@Override
public RespBean login(String username, String password, HttpServletRequest request) {
// 登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails==null || !userDetails.getPassword().equals(password)) {
return RespBean.error("用户名或者密码不正确");
}
if (!userDetails.isEnabled()) {
return RespBean.error("账号被禁用,请练习管理员");
}
// 更新security登录用户对象
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new
UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());//第二个参数为口令(密码)不用传
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
// 生成token
String token = jwtTokenUtil.generateToken(userDetails);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return RespBean.success("登录成功", tokenMap);
}
}
3.2获取用户的信息开发
1.获取用户信息的Controller层(和登录在同一个Controller)
⭐ Principal对象: principal是我们设置到security中存的全局的用户信息
import *;
/**
* 登录
*/
@Api(tags = "LoginController")
@RestController
public class LoginController {
@Autowired
private IAdminService adminService;
@ApiOperation(value = "获取当前登录用户的信息")
@GetMapping("/admin/info")
public Admin getAdminInfo(Principal principal) {
// principal是我们设置到security中存的全局的用户信息
if (principal == null) {
return null;
}
String username = principal.getName();
Admin admin = adminService.getAdminByUserName(username);
admin.setPassword(null);
return admin;
}
@ApiOperation(value = "用户退出登录")
@PostMapping("/logout")
private RespBean logout(){
return RespBean.success("注销成功!");
}
}
2. 获取用户信息的Service
- 接口层
/**
* 查询当前用户信息
* @param username
* @return
*/
RespBean getAdminByUserName(String username);
- 实现层
⭐ adminMapper.selectOne(new QueryWrapper<Admin>().eq(“username”, username).eq(“enable”, true));
这里是Mybatis-Plus中获取Admin实体类对应数据库表的数据,条件可以链式判断eq(), 两个参数前面的是指定列,后面是判断该列的值
/**
* <p>
* 服务实现类
* </p>
*
* @author seven
* @since 2022-01-02
*/
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder; // 判断密码是否一致
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHead}") // 根据value注解拿到yml中设置的token头
private String tokenHead;
@Autowired
private AdminMapper adminMapper;
/**
* 获取当前用户信息
* @param username
* @return
*/
@Override
public Admin getAdminByUserName(String username) {
return adminMapper.selectOne(new QueryWrapper<Admin>().eq("username", username).eq("enable", true));
}
}
3.3 Security的配置类
1. SecurityConfig配置
- 我们主要配置了通过jwt我们无序使用csrf, 以及禁用了cache,使用cookie技术存token。(通过重写configure)
- 添加jwt登录过滤器以及未授权或未登录返回结果
- 配置放行接口
在配置类下方是注入的一些Bean,包括我们自己用到的passwordEncoder、userDetailsService。以及jwt登录过滤与未授权和未登录的结果返回
package com.jzq.server.config.security;
import com.jzq.server.pojo.Admin;
import com.jzq.server.service.IAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.Filter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private IAdminService adminService;
@Override
public void configure(WebSecurity web) throws Exception {
// 设置放行访问
web.ignoring().antMatchers(
"/login",
"/logout",
"/css/**",
"/js/**",
"/index.html",
"/favicon.ico",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**"
);
}
@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception{
authenticationManagerBuilder.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 使用JWT,不需要csrf
http.csrf()
// 禁用csrf
.disable()
// 基于token, 不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 允许登录访问
//.antMatchers("/login", "/logout")
//.permitAll()
// 除了上面的请求,其他的都要认证
.anyRequest()
.authenticated()
.and()
// 禁用缓存
.headers()
.cacheControl();
// 添加 JWT 登录授权过滤器
http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
// 添加自定义未授权和未登录结果的返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandel())
.authenticationEntryPoint(restAuthorizationEnrtyPoint());
}
@Override
@Bean
public UserDetailsService userDetailsService() { // 重写的了返回的参数(Admin)
return username -> {
Admin admin = adminService.getAdminByUserName(username);
if (admin != null) {
return admin;
}
return null;
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthencationTokenFilter jwtAuthencationTokenFilter() {
return new JwtAuthencationTokenFilter();
}
@Bean
public RestAuthorizationEnrtyPoint restAuthorizationEnrtyPoint() {
return new RestAuthorizationEnrtyPoint();
}
@Bean
public RestfulAccessDeniedHandel restfulAccessDeniedHandel() {
return new RestfulAccessDeniedHandel();
}
}
2. Jwt登录授权过滤器实现
通过获取token后判断tusername是否存在,以及是否设置到Security全局中,验证token的有效性从而设置到security的全局中。
package com.jzq.server.config.security;
import lombok.experimental.Accessors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* JWT登录授权过滤器
*/
public class JwtAuthencationTokenFilter extends OncePerRequestFilter {
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(tokenHeader); // 获取token值
// 存在token (获取的token不为空,且token前面携带的表示是我们设置的标识)
if (authHeader!=null && authHeader.startsWith(tokenHead)){
String authToken = authHeader.substring(tokenHead.length()); // 截取下除了头部标识以后的token
String username = jwtTokenUtil.getUserNameFromToken(authToken); // 通过token就可以拿到用户名(解析荷载部分)
// 看看username存在,但是未登录(就是检测是不是设置在security全局中)
if (username != null && SecurityContextHolder.getContext().getAuthentication()==null) {
// 登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 验证token是否有效,重新设置用户对象
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
// 最后放行
filterChain.doFilter(request,response);
}
}
3. 未授权结果返回
package com.jzq.server.config.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jzq.server.config.result.RespBean;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 当访问接口没有权限的时候,自定义返回结果
*/
public class RestfulAccessDeniedHandel implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter printWriter = response.getWriter();
RespBean respBean = RespBean.error("权限不足,请联系管理员");
respBean.setCode(403);
printWriter.write(new ObjectMapper().writeValueAsString(respBean));
printWriter.flush(); // 强推到浏览器
printWriter.close();
}
}
4. 未登录或登录失效返回
package com.jzq.server.config.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jzq.server.config.result.RespBean;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Controller;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 当未登录或token失效的时候访问接口时,返回的自定义结果
*/
@Controller
public class RestAuthorizationEnrtyPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter printWriter = response.getWriter();
RespBean respBean = RespBean.error("登录失效,请重新登录");
respBean.setCode(401);
printWriter.write(new ObjectMapper().writeValueAsString(respBean));
printWriter.flush(); // 强推到浏览器
printWriter.close();
}
}
四、Swagger2配置
4.1 pom
<!-- swagger2 依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<!-- Swagger第三方ui依赖 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
4.2 swagger配置类
配置了文档访问地址
以及配置了全局配置Authorization
package com.jzq.server.config.Swagger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
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.List;
/**
* Swagger2配置
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.jzq.server.controller"))
.paths(PathSelectors.any())
.build()
.securityContexts(securityContexts())
.securitySchemes(securitySchemes());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("云e办接口文档")
.description("描述")
.contact(new Contact("jzq", "http:localhost:7777/doc.html", "xxx@xxx.com"))
.build();
}
private List<ApiKey> securitySchemes() {
// 设置请求头
List<ApiKey> result = new ArrayList<>();
ApiKey apiKey = new ApiKey("Authorization", "Authorization", "Header");
result.add(apiKey);
return result;
}
private List<SecurityContext> securityContexts() {
// 设置需要登录认证的路径
List<SecurityContext> result = new ArrayList<>();
result.add(getContextByPath("/test/.*"));
return result;
}
private SecurityContext getContextByPath(String pathRegex) {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(pathRegex))
.build();
}
private List<SecurityReference> defaultAuth() {
List<SecurityReference> result = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
result.add(new SecurityReference("Authorization", authorizationScopes));
return result;
}
}
例如: