目录:
(1)搭建环境、JWT工具类
(2)返回公共对象
(3)登录之后返回token
(4)获取当前登录用户的信息
(5)配置Security登录授权过滤器
(6)Security自定义返回结果
(7)Swagger2配置
Swagger的简介
(8)Swagger2添加Authorize
(9)生成验证码
(10)校检验证码
(11)根据用户id查询菜单列表
现在通过逆向工程,把项目中需要使用到的一些pojo类,Mapper接口、xml、service接口、controller等等创建好之后,全部放到yeb-server项目里面 ,接下来正是开发功能了
前面yub项目搭建好之后:
现在开始做第一个功能,采用SpringSecurity安全框架、JWT令牌、来实现登录功能:
(1)搭建环境、JWT工具类
首先引入依赖:
Swagger:依赖使用了国人基于bootstrap-第三方UI界面,比较符合国人的习惯
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--关联父工程-->
<parent>
<artifactId>yeb</artifactId>
<groupId>com.xxx</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.xxxx</groupId>
<artifactId>yun-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!--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>
</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 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--JWT 依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
做一些JWT的配置:
在配置文件application.yml
server:
port: 8081
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: 123456
hikari:
#连接池名
pool-name: DateHikarCP
#最小空闲连接
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: SELECT 1
mybatis-plus:
#配置Mapper映射文件
mapper-locations: classpath*:/mapper/*Mapper.xml
#配置mybatis数据返回类型别名
type-aliases-package: com.xxxx.server.pojo
configuration:
# 自动驼峰命名
map-underscore-to-camel-case: false
## Mybatis SQL打印(方法接口所在的包,不是Mapper.xml所在的包)
logging:
level:
com.xxxx.server.mapper: debug
jwt:
# JWT存储的请求头
tokenHeader: Authorization
# JWT 加解密使用的密钥
secret: yeb-secret
# JWT的超期限时间(60*60*24)
expiration: 604800
# JWT 负载中拿到开头
tokenHead: Bearer
准备JWT工具类,方便JWT有关的功能去使用,直接用工具类去完成:
JwtTockenUtil:
package com.xxxx.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;
/**
* JwtToken工具类
*
* @author zhoubin
* @since 1.0.0
*/
@Component
public class JwtTokenUtil {
private static final String CLAIM_KEY_USERNAME = "sub";//荷载里面用户名的key
private static final String CLAIM_KEY_CREATED = "created";//JWT的创建使用 也是放到荷载头里面,准备key
@Value("${jwt.secret}")
private String secret;//秘钥,通过注解从yml文件中去拿
@Value("${jwt.expiration}")
private Long expiration;//失效时间,通过注解去拿
/**
* 根据用户信息生成token
*
* @param userDetails
* @return
*/
//用户信息从SpringSecurity框架里的UserDetails去拿取
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());
//调用下面私有方法4:
return generateToken(claims);
}
/**
* 从token中获取登录用户名
* @param token
* @return
*/
public String getUserNameFromToken(String token){
String username;
try {
//调用下面私有方法3,先从token获取荷载,用户名是放在荷载里面的
Claims claims = getClaimsFormToken(token);
//获取主体用户名
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 验证token是否有效
* @param token
* @param userDetails
* @return
*/
public boolean validateToken(String token,UserDetails userDetails){
//调用上面的方法获取用户名
String username = getUserNameFromToken(token);
//判断token里面的用户名和UserDetaile里面的用户名是否一致,和token是否失效,调用下面私有方法1
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token是否可以被刷新
* @param token
* @return
*/
public boolean canRefresh(String token){
return !isTokenExpired(token);
}
/**
* 刷新token
* @param token
* @return
*/
public String refreshToken(String token){
Claims claims = getClaimsFormToken(token);
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims);
}
/**
* 私有方法1:
* 判断token是否失效
* @param token
* @return
*/
private boolean isTokenExpired(String token) {
//调用下面私有方法2:
Date expireDate = getExpiredDateFromToken(token);
return expireDate.before(new Date());
}
/**
* 私有方法2:
* 从token中获取过期时间
* @param token
* @return
*/
private Date getExpiredDateFromToken(String token) {
//调用私有方法3获取荷载
Claims claims = getClaimsFormToken(token);
return claims.getExpiration();
}
/**
* 私有方法3:
* 从token中获取荷载
* @param token
* @return
*/
private Claims getClaimsFormToken(String token) {
Claims claims = null;
try {
//获取荷载对象
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
/**
* 私有方法4:
* 根据荷载生成JWT TOKEN
*
* @param claims
* @return
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)//放入荷载
.setExpiration(generateExpirationDate())//失效时间
.signWith(SignatureAlgorithm.HS512, secret)//签名
.compact();
}
/**
* 私有方法5:
* 生成token失效时间
*
* @return
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
}
(2)返回公共对象
整体的流程,前端传用户名和密码给我们,后端先去校验用户名和密码,如果用户名和密码有错误,我们让他直接输入,如果正确的话的我们应该生成JWT令牌,并且返回给前端,前端拿到JWT令牌之后呢,就会放到它的请求头里面,后面的任何请求都会携带JWT令牌,后端呢会有拦截器对JWT令牌作相应的验证,验证访问通过呢,才能访问对应的接口,如果JWT验证不通过,就说明JWT失效了要么你这个用户名有问题或者面有问题,反正不是一个合法登录的用户,没办法让你访问对应的接口
项目中会有一些公共的返回东西,想准备公共的返回对象:
在pojo目录下创建:RespBean
package com.xxxx.server.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 公共返回类
*
* @author zhanglishen
* @since 1.0.0
*/
@Data //Lombok注解
@NoArgsConstructor //无参构造
@AllArgsConstructor //有参构造
public class RespBean {
private long code;//状态码
private String message;//提示信息
private Object obj;//返回对象
//成功返回结果
public static RespBean success(String message){
return new RespBean(200,message,null);
}
//重写成功返回结果方法
public static RespBean success(String message,Object obj){
return new RespBean(200,message,obj);
}
//失败返回结果
public static RespBean error(String message){
return new RespBean(500,message,null);
}
public static RespBean error(String message,Object obj){
return new RespBean(500,message,obj);
}
}
(3)登录之后返回token
登录之后的流程,我们用到SpringSecurity+JWT 正常情况下前端输入用户名和密码,传给我们,我们去判断用户名和密码是否正确,如果用户名和密码正确的话呢我们会给它生成一个JWT令牌,返回给前端,如果用户名和密码不正确呢我们让它重新输入 ,前端拿到JWT 令牌之后呢会把它放到请求头里面,后面的每一次请求都会携带JWT令牌,我们需要写一个JWT令牌拦截器,每次请求之前都会走一遍拦截器,去判断JWT令牌是都存在以及JWT令牌是否正确、合法有效,如果不存在以及合法有效JWT令牌,我们就拦截掉了,不让他访问其他的接口,如果判断出来JWT令牌是合法有效的,我们就允许他访问其他的接口,我们先实现第一个步骤,前端创用户名和密码,我们根据用户名密码,是否正确,正确的话给它生成一个JWT令牌,在Admin里面实现接口:UserDetails
SpringSecurity框架登录的方法就是UserDetailService里面的LoadUserByUsename通过这个方法去登录的,登录之后返回的就是UserDetails
package com.xxxx.server.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
/**
* <p>
*
* </p>
*
* @author zhanglishen
* @since 2022-07-31
*/
@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 = "是否启用")
private Boolean enabled;
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "用户头像")
private String userFace;
@ApiModelProperty(value = "备注")
private String remark;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
在去新建一个pojo类:这个类专门用来传递前端传递的用户名和密码,我们没必要用Admin对象,因为Admin里面有很多很多参数,而登陆的话它只需要两个参数用户名和密码,其他的用不到
pojo目录下:AdminLoginParam:这个后期名字可能更改
package com.xxxx.server.pojo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户登录实体类
*
* @author zhanglishen
* @since 1.0.0
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Api(value = "AdminLogin对象") //swagger、注解接口文档 前后端分离
public class AdminLoginParam {
@ApiModelProperty(value = "用户名",required = true)
private String username;
@ApiModelProperty(value = "密码",required = true)
private String password;
@ApiModelProperty(value = "验证码",required = true)
private String code;
}
从前往后写:新建LoginController:
package com.xxxx.server.controller;
import com.xxxx.server.pojo.AdminLoginParam;
import com.xxxx.server.pojo.RespBean;
import com.xxxx.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") //Swagger注解文档
@RestController
public class LoginController {
//注入service
@Autowired
private IAdminService adminService;
@ApiOperation(value = "登录之后返回token")
@PostMapping("/login")
public RespBean login(AdminLoginParam adminLoginParam, HttpServletRequest request){
return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),request);
}
}
IAadminService接口:
package com.xxxx.server.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xxxx.server.pojo.Admin;
import com.xxxx.server.pojo.RespBean;
import javax.servlet.http.HttpServletRequest;
/**
* <p>
* 服务类
* </p>
*
* @author zhanglishen
* @since 2022-08-05
*/
public interface IAdminService extends IService<Admin> {
//登录方法 登录之后返回token
RespBean login(String username, String password, HttpServletRequest request);
//根据用户名获取用户
Admin getAdminByUserName(String username);
}
AdmainSereviceImpl:实现类
SpringSecurity通过UserDetailService中的LoadUserByUserName来实现的
package com.xxxx.server.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xxxx.server.config.security.JwtTokenUtil;
import com.xxxx.server.mapper.AdminMapper;
import com.xxxx.server.pojo.Admin;
import com.xxxx.server.pojo.RespBean;
import com.xxxx.server.service.IAdminService;
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 zhanglishen
* @since 2022-08-05
*/
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {
//用到查数据库需要注入
@Autowired
private AdminMapper adminMapper;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
//注入工具类
@Autowired
private JwtTokenUtil jwtTokenUtil;
//通过注解去拿头部信息
@Value("${jwt.tokenHead}")
private String tokenHead;
//登录之后返回token
@Override
public RespBean login(String username, String password, HttpServletRequest request) {
//登录
//获取到userDetaiils
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (null==userDetails||!passwordEncoder.matches(password,userDetails.getPassword())){
return RespBean.error("用户名或密码不正确");
}
if (!userDetails.isEnabled()){
return RespBean.error("账号被禁用!请联系管理员");
}
//更新security登录用户对象,把userDetails对象放到security全文中
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//生成token
//调用工具类的方法拿到token
String token = jwtTokenUtil.generateToken(userDetails);
Map<String,String> tokenMap=new HashMap<>();
tokenMap.put("token",token);
tokenMap.put("tokenHead",tokenHead);
return RespBean.success("登录成功",tokenMap);//把token返回给前端
}
//根据用户名获取
@Override
public Admin getAdminByUserName(String username) {
//使用MyBatis-plus查询
return adminMapper.selectOne(new QueryWrapper<Admin>().eq("usernaem",username).eq("enabled",true));
}
}
(4)获取当前登录用户的信息
先写退出功能:和前端定义好了,前端处理退出的功能,后端只要返回成功的接口就行了
它调退出登录的接口,我们返回它退出成功,主要拿一个状态码200,前端拿状态码200之后呢前端直接在他的请求头里面把tcken删除,它在调用接口呢就会被拦截器拦截,不让它访问
package com.xxxx.server.controller;
import com.xxxx.server.pojo.Admin;
import com.xxxx.server.pojo.AdminLoginParam;
import com.xxxx.server.pojo.RespBean;
import com.xxxx.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.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
@Api(tags = "LoginController") //Swagger注解文档
@RestController
public class LoginController {
//注入service 接口
@Autowired
private IAdminService adminService;
@ApiOperation(value = "登录之后返回token")
@PostMapping("/login")
public RespBean login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request){
//调用接口IAdminService接口类中的login方法
return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),request);
}
@ApiOperation(value = "获取当前登录用户的信息")
@GetMapping("/admin/info")
public Admin getAdminInfo(Principal principal){
if (null == principal) {
return null;
}
//用户名
String username = principal.getName();
//根据用户名获取完整的对象
Admin admin=adminService.getAdminByUserName(username);
admin.setPassword(null);
return admin;
}
@ApiOperation(value = "退出登录")
@PostMapping("/logout")
public RespBean logout(){
return RespBean.success("注销成功");
}
}
(5)配置Security登录授权过滤器
在config.security包下创建SecurityConfig:
配置JWT令牌拦截器:
创建JwtAuthenticationTokenFilter:
package com.xxxx.server.config.security;
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.stereotype.Component;
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登录认证过滤器
*
* @author zhanglishen
*/
public class JwtAuthenticationTokenFilter 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 httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
//获取JWT请求头
String header = httpServletRequest.getHeader(tokenHeader);
//存在token
if (header != null && header.startsWith(tokenHead)){
String authToken = header.substring(tokenHead.length());//获取token
String username = jwtTokenUtil.getUserNameFromToken(authToken);
//token存在,但是未登录
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(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
(6)Security自定义返回结果
在security目录下创建:RestAuthorizationEntryPoint、RestfulAccessDeniedHandler
package com.xxxx.server.config.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxxx.server.pojo.RespBean;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 当未登录或者token实效是访问接口时,自定义返回结果
*/
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
PrintWriter out = response.getWriter();
RespBean bean = RespBean.error("RestAuthorizationEntryPoint + 未登录!");
bean.setCode(401);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}
package com.xxxx.server.config.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxxx.server.pojo.RespBean;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 当访问接口没有权限时,自定义返回结果
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
PrintWriter out = response.getWriter();
RespBean bean = RespBean.error("RestfulAccessDeniedHandler + 权限不足!");
bean.setCode(401);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}
在SecurityConfig:注入以下
package com.xxxx.server.config.security;
import com.xxxx.server.pojo.Admin;
import com.xxxx.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.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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private IAdminService adminService;
//注入以下刚创建的两个类
@Autowired
private RestAuthorizationEntryPoint restAuthorizationEntryPoint;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
//security走我们重写的UserDetails
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//2.把自己重写的UserDetail放进来,需要passwordEncorder下面注入进来
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
//3. springsecurity的配置
@Override
protected void configure(HttpSecurity http) throws Exception {
//使用JWT不需要csrf
http.csrf()
.disable()
//基于token,不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//允许访问登录
.authorizeRequests()
.antMatchers("/login","/logout")
.permitAll()
//除了上面的请求,所有请求需要被拦截
.anyRequest()
.authenticated()
.and()
//禁用缓存
.headers()
.cacheControl();
//4.添加jwt授权拦截器
http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//5.添加自定义未授权、未定义结果的返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);
}
// 1.覆盖重写UserDetailsService 里面的LoadUserByUserName
@Override
@Bean
public UserDetailsService userDetailsService(){
return username -> {
Admin admin = adminService.getAdminByUserName(username);
if (null!=admin){
return admin;
}
return null;
};
}
//暴露PasswordEncoder对象
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//暴露出来对象
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
return new JwtAuthenticationTokenFilter();
}
}
(7)Swagger2配置
去写其他的接口,先配置接口文档
因为这是前后端分离的项目,后端写完接口之后,要把对应的接口文档整理好,给前端,然后前端在根据接口文档去开发,我们现在已经抛弃了用多口文档的方式,都是用Swagger ,因为Swagger优点比较多,它会自动根据代码生成接口文档同时他的接口文档还提供了接口调试的功能,相当于把postman都替代了,所以Swagger非常强大
Swagger的简介:
swagger-ui
用来显示API文档的,不可编辑,会根据我们在代码中的设置来自动生成Api说明文档。生成的api文档如下,不好意思,长得不太像文档…但比文档更清楚对不对!对!
Swagger UI 简介
Swagger UI允许任何人(无论您是开发团队还是最终用户)都可以可视化API资源并与之交互,而无需任何实现逻辑。它是根据您的OpenAPI(以前称为Swagger)规范自动生成的,具有可视化文档,可简化后端实现和客户端使用。
简单地使用swagger-ui只需要三步。
第一步,配置pom文件。在pom文件中引入swagger的相关依赖,在
“<dependencies></dependencies>”中间添加下面的依赖。
<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.8.0</version> </dependency>
第二步,构建swagger配置类。我选择构建的位置是主目录下,目录并不会对运行结果产生影响,但整个项目只能有一个swagger配置类。
import io.swagger.annotations.Api; 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.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; /** * Swagger使用的配置文件 */ @Configuration @EnableSwagger2 public class Swagger2Configuration { @Bean public Docket createRestApi(){ return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.withClassAnnotation(Api.class)) .paths(PathSelectors.any()) .build(); } //基本信息的配置,信息会在api文档上显示 private ApiInfo apiInfo(){ return new ApiInfoBuilder() .title("zg测试的接口文档") .description("xx相关接口的文档") .termsOfServiceUrl("http://localhost:8080/hello") .version("1.0") .build(); } }
apiInfo:api基本信息的配置,信息会在api文档上显示,可有选择的填充,比如配置文档名称、项目版本号等
apis:使用什么样的方式来扫描接口,RequestHandlerSelectors下共有五种方法可选。我们当前使用通过在类前添加@Api注解的方式,其他方法我们后续介绍。
path:扫描接口的路径,PathSelectors下有四种方法,我们这里是全扫,其他方法我们后续介绍。
第三步,在接口文件中增加对应注解。代码如下,由于我们第二步选择扫描接口的方式是在类前添加@Api;@ApiOperation用于注明接口,value是接口的解释;@ApiParam注解函数里面的参数,name一般与参数名一致,value是解释,required是是否参数必须。@Controller @Api public class HelloController { @ApiOperation(value = "你好") @ResponseBody @PostMapping("/hello") public String hello(@ApiParam(name="name",value="对话人",required=true)String name){ return name+", hello"; } }
上面操作都完成后,在浏览器中输入网址:http://localhost:8080/swagger-ui.html,我使用的接口是默认的8080,具体以自己项目的配置为准。
swagger api注解
这一部分除了,下面列出的注解外,还包括上面所介绍的RequestHandlerSelectors和PathSelectors的几种方法及含义。
@Api: 用于类,标识这个类是swagger的资源
@ApiIgnore: 用于类,忽略该 Controller,指不对当前类做扫描
@ApiOperation: 用于方法,描述 Controller类中的 method接口
@ApiParam: 用于参数,单个参数描述,与 @ApiImplicitParam不同的是,他是写在参数左侧的。如( @ApiParam(name="username",value="用户名")Stringusername)
@ApiModel: 用于类,表示对类进行说明,用于参数用实体类接收
@ApiProperty:用于方法,字段,表示对model属性的说明或者数据操作更改
@ApiImplicitParam: 用于方法,表示单独的请求参数
@ApiImplicitParams: 用于方法,包含多个 @ApiImplicitParam
@ApiResponse: 用于方法,描述单个出参信息
@ApiResponses: 用于方法,包含多个@ApiResponse
@ApiError: 用于方法,接口错误所返回的信息
SwaggerConfig:Swagger配置
package com.xxxx.server.config;
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.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* Swagger配置
*
* @author zahnglishen
* @since 1.0.0
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//包扫描
.apis(RequestHandlerSelectors.basePackage("com.xxxx.server.controller"))
.paths(PathSelectors.any())
.build();
//.securitySchemes(securitySchemes())
//.securityContexts(securityContexts());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("云e办接口文档")
.description("接口文档")
.contact(new Contact("zhanglishen", "http://localhost:8081/doc.html", "xxxx@xxxx.com"))
.version("1.0")
.build();
}
}
在Contrller目录下:下一个Controller,测试接口用的,创建HelloController:
package com.xxxx.server.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
//测试
@RestController
public class HelloController {
@GetMapping("hello")
public String hello(){
return "hello";
}
}
在SecurityConfig放行接口文档:
package com.xxxx.server.config.security;
import com.xxxx.server.pojo.Admin;
import com.xxxx.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.context.annotation.Lazy;
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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Lazy
@Autowired
private IAdminService adminService;
//注入以下刚创建的两个类
@Autowired
private RestAuthorizationEntryPoint restAuthorizationEntryPoint;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
//security走我们重写的UserDetails
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//2.把自己重写的UserDetail放进来,需要passwordEncorder下面注入进来
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
//6.设置放行一些路径,不走拦截连
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/login",
"/logout",
"/CSS/**",
"/js/**",
"/index.html",
"favicon.ico",
"/doc.html",
"/webjars/**",//放行Swagger接口文档
"/swagger-resources/**",
"/v2/api-docs/**"
);
}
//3. springsecurity的配置
@Override
protected void configure(HttpSecurity http) throws Exception {
//使用JWT不需要csrf
http.csrf()
.disable()
//基于token,不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//允许访问登录
.authorizeRequests()
//.antMatchers("/login","/logout")
//.permitAll()
//除了上面的请求,所有请求需要被拦截
.anyRequest()
.authenticated()
.and()
//禁用缓存
.headers()
.cacheControl();
//4.添加jwt授权拦截器
http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//5.添加自定义未授权、未定义结果的返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);
}
// 1.覆盖重写UserDetailsService 里面的LoadUserByUserName
@Override
@Bean
public UserDetailsService userDetailsService(){
return username -> {
//调用IAdminService接口中的获取用户信息方法
Admin admin = adminService.getAdminByUserName(username);
if (null!=admin){
return admin;
}
return null;
};
}
//暴露PasswordEncoder对象
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//暴露出来对象
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
return new JwtAuthenticationTokenFilter();
}
}
运行主启动类:
再地址栏输入:进入SwaggerUI界面
还可以调试:点击发送显示的还是需要登录,因为上面只放行了login、logout、和静态资源...其他请求需要登录之后才能访问
(8)Swagger2添加Authorize
Security放行的就上面那几个,静态资源、登录登出,其他的正常情况下都是要求用户登录之后才能访问的接口 ,那问题是要登录才能访问接口,那接口文档怎么办?不可能说我现在在接口文档里面了我先把它放行,然后再测试,测试完之后再把它关上,这个过于繁琐了,那怎么办呢?其实Swagger提供了全局登录的功能,我们现在用的是JWT登录,,登录之后返回JWT令牌,这个令牌放到前端请求头里面呢,那么我们Swagger文档呢,也可以这样的我们先登录,登录之后呢,我们把登录放到全局的Authorization里面,然后呢你这个文档在访问Hello接口的时候,他就会携带JWT令牌,携带了JWT令牌之后呢,我们刚才写过JWT令牌授权拦截器,如果它有JWT令牌的话,我们会自动给它登录,就可以访问我们想要的一个接口了,下面给Swagger加上全局的Authorization
Swagger2Config:
package com.xxxx.server.config;
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;
/**
* Swagger配置
*
* @author zahnglishen
* @since 1.0.0
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//包扫描
.apis(RequestHandlerSelectors.basePackage("com.xxxx.server.controller"))
.paths(PathSelectors.any())
.build()
//全局作用
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("云e办接口文档")
.description("接口文档")
.contact(new Contact("zhanglishen", "http://localhost:8081/doc.html", "xxxx@xxxx.com"))
.version("1.0")
.build();
}
private List<ApiKey> securitySchemes() {
//设置请求头信息
List<ApiKey> apiKeys = new ArrayList<>();
apiKeys.add(new ApiKey("Authorization", "Authorization", "header"));
return apiKeys;
}
private List<SecurityContext> securityContexts() {
//设置需要登录认证的路径
List<SecurityContext> list = new ArrayList<>();
list.add(getContextByPath("/hello/.*"));
return list;
}
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;//把authorizationScope放进数组
result.add(new SecurityReference("Authorization", authorizationScopes));
return result;
}
}
上面是form表单登录 ,使用json格式登录: 需要在 LoginController:更该一点:加RequestBody注解
SecurityConfig:注释掉,上面已经放行了一些路径了这里就不用了
运行:选择LoginContrller中的:登录之后返回token
点击发送:
{
"code": 200,
"message": "登录成功",
"obj": {
"tokenHead": "Bearer",
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE2NTk3NTYzMDc0NjUsImV4cCI6MTY2MDM2MTEwN30.IvIrqy1LUlXPUYSeLl7eOkdjTTF7UxzXpjkr733uQ6uxPqsnYm70wkezHR23eMtdUJFppSXU9efLZAWueDGt3w"
}
}
把生成的信息在:点击Authorze 添加参数
Bearer +eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE2NTk3NTYzMDc0NjUsImV4cCI6MTY2MDM2MTEwN30.IvIrqy1LUlXPUYSeLl7eOkdjTTF7UxzXpjkr733uQ6uxPqsnYm70wkezHR23eMtdUJFppSXU9efLZAWueDGt3w
点击保存:保存成功之后就可以访问任何的接口了,他都会携带JWT令牌
保存完成之后,他去访问所有的接口,都会携带这个东西,相当于请求头,就相当于接口文档已经登录了
获取当前登录用户信息:选择调试-发送:就获取到了
访问测试接口:hello也拿到了
(9)生成验证码
我们已经包把登录已经处理好了,接口文档也能实现登录之后访问对应的接口,下面给登录补充一点功能,比如验证码,现在市面上流行的验证码比较多,比如,普通的图形验证码、滑动验证、比如12306给你一个汉字词语让你去图片找对应的、谷歌的图形验证码,我们这里使用谷歌的一个验证码Capther这是比较简单的验证码,就是正常的传统的4到5位字母+数字形成的验证码,因为我们现在是后台的系统,不需要那么复杂的
引入依赖:
创建谷歌验证码配置类:
CaptchaConfig:
package com.xxxx.server.config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* 验证码配置
*
* @author zhanglishen
* @since 1.0.0
*/
@Configuration
public class CaptchaConfig {
@Bean
public DefaultKaptcha getDefaultKaptcha() {
//验证码生成器
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
//配置
Properties properties = new Properties();
//是否有边框
properties.setProperty("kaptcha.border", "yes");
//设置边框颜色
properties.setProperty("kaptcha.border.color", "105,179,90");
//边框粗细度,默认为1
// properties.setProperty("kaptcha.border.thickness","1");
//验证码
properties.setProperty("kaptcha.session.key", "code");
//验证码文本字符颜色 默认为黑色
properties.setProperty("kaptcha.textproducer.font.color", "blue");
//设置字体样式
properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅 黑");
//字体大小,默认40
properties.setProperty("kaptcha.textproducer.font.size", "30");
//验证码文本字符内容范围 默认为abced2345678gfynmnpwx
// properties.setProperty("kaptcha.textproducer.char.string", "");
//字符长度,默认为5
properties.setProperty("kaptcha.textproducer.char.length", "4");
//字符间距 默认为2
properties.setProperty("kaptcha.textproducer.char.space", "4");
//验证码图片宽度 默认为200
properties.setProperty("kaptcha.image.width", "100");
//验证码图片高度 默认为40
properties.setProperty("kaptcha.image.height", "40");
//生成Config
Config config = new Config(properties);
//把config放入验证码生成器里面
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
去写接口,在controller包下创建CaptchaController:
package com.xxxx.server.controller;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
@RestController
public class CaptchaController {
//注入DefaultKaptcha
@Autowired
private DefaultKaptcha defaultKaptcha;
@ApiOperation(value = "验证码") //Swagger注解注释
@GetMapping(value = "/captcha",produces = "image/jpeg") //produces = "image/jpeg"排除在接口文档中不能正常显示图片的乱码问题,设置接口文档输出的是一张图片
public void captcha(HttpServletRequest request, HttpServletResponse response){
//以流的形式传图片过去,response响应头做一些处理
//定义response输出类型为image/jpeg
response.setDateHeader("Expires",0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
//---------------------------生成验证码 begin----------------------
//获取验证码文本内容
String text = defaultKaptcha.createText();
System.out.println("验证码: " + text);
//将验证码放到session中
request.getSession().setAttribute("captcha",text);
//根据文本内容创建图形验证码
BufferedImage image = defaultKaptcha.createImage(text);
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
//输出流输出图片,格式为jpg
ImageIO.write(image,"jpg",outputStream);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (outputStream != null){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//------------------------end-----------------
}
}
验证码登录之前就要获取,所以需要在SecurityConfig配置类进行放行:
package com.xxxx.server.config.security;
import com.xxxx.server.pojo.Admin;
import com.xxxx.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.context.annotation.Lazy;
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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Lazy
@Autowired
private IAdminService adminService;
//注入以下刚创建的两个类
@Autowired
private RestAuthorizationEntryPoint restAuthorizationEntryPoint;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
//security走我们重写的UserDetails
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//2.把自己重写的UserDetail放进来,需要passwordEncorder下面注入进来
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
//6.设置放行一些路径,不走拦截连
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/login",
"/logout",
"/CSS/**",
"/js/**",
"/index.html",
"favicon.ico",
"/doc.html",
"/webjars/**",//放行Swagger接口文档
"/swagger-resources/**",
"/v2/api-docs/**",
"/captcha"
);
}
//3. springsecurity的配置
@Override
protected void configure(HttpSecurity http) throws Exception {
//使用JWT不需要csrf
http.csrf()
.disable()
//基于token,不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//允许访问登录
.authorizeRequests()
//.antMatchers("/login","/logout")
//.permitAll()
//除了上面的请求,所有请求需要被拦截
.anyRequest()
.authenticated()
.and()
//禁用缓存
.headers()
.cacheControl();
//4.添加jwt授权拦截器
http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//5.添加自定义未授权、未定义结果的返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);
}
// 1.覆盖重写UserDetailsService 里面的LoadUserByUserName
@Override
@Bean
public UserDetailsService userDetailsService(){
return username -> {
//调用IAdminService接口中的获取用户信息方法
Admin admin = adminService.getAdminByUserName(username);
if (null!=admin){
return admin;
}
return null;
};
}
//暴露PasswordEncoder对象
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//暴露出来对象
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
return new JwtAuthenticationTokenFilter();
}
}
在浏览器地址输入:访问接口文档:http://localhost:8081/doc.html
点击刚创建的captcha-controller
(10)校检验证码
验证码接口接口已经有了,添加校验码的参数,刚开始的时候用户名、和密码专门有一个类去接收的是AdminLoginParam类,现在让它多接受一个验证码
在登录返回token的接口加参数:
在AdminServiceImpl:中做校验
当时我们生成了有个验证码之后呢吧它放到了Session里面,现在可以去获取:
package com.xxxx.server.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xxxx.server.config.security.JwtTokenUtil;
import com.xxxx.server.mapper.AdminMapper;
import com.xxxx.server.pojo.Admin;
import com.xxxx.server.pojo.RespBean;
import com.xxxx.server.service.IAdminService;
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 org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* 服务实现类
* </p>
*
* @author zhanglishen
* @since 2022-08-05
*/
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {
//用到查数据库需要注入
@Autowired
private AdminMapper adminMapper;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
//注入工具类
@Autowired
private JwtTokenUtil jwtTokenUtil;
//通过注解去拿头部信息
@Value("${jwt.tokenHead}")
private String tokenHead;
//登录之后返回token
@Override
public RespBean login(String username, String password, String code, HttpServletRequest request) {
//获取验证码
String captcha =(String) request.getSession().getAttribute("captcha");
//判断 如果输入验证码为空或者输入的验证码不正确
if (StringUtils.isEmpty(code)||!captcha.equalsIgnoreCase(code)){
return RespBean.error("验证码输入错误,请重新输入!");
}
//登录
//获取到userDetaiils
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (null==userDetails||!passwordEncoder.matches(password,userDetails.getPassword())){
return RespBean.error("用户名或密码不正确");
}
if (!userDetails.isEnabled()){
return RespBean.error("账号被禁用!请联系管理员");
}
//更新security登录用户对象,把userDetails对象放到security全文中
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//生成token
//调用工具类的方法拿到token
String token = jwtTokenUtil.generateToken(userDetails);
Map<String,String> tokenMap=new HashMap<>();
tokenMap.put("token",token);
tokenMap.put("tokenHead",tokenHead);
return RespBean.success("登录成功",tokenMap);//把token返回给前端
}
//根据用户名获取
@Override
public Admin getAdminByUserName(String username) {
//使用MyBatis-plus查询
return adminMapper.selectOne(new QueryWrapper<Admin>().eq("username",username).eq("enabled",true));
}
}
先点击接口,产生验证码:
把成功返回后的令牌tcken,重新更改 Authorize
访问前端页面:验证码正常显示
(11)根据用户id查询菜单列表
实现菜单功能:首先要考虑几点菜单可能设计到权限,不同用户,有不同的角色,不同角色会有不同的权限,不同的权限看到的菜单列表是不一样的,但这个呢后面讲到权限在去处理,现在先做简单根据当前登录的用户id去查对应的菜单列表,我们先来看菜单表:
t_menu_role表:要求的权限
t_role:角色表对应t_admin用户表 用户也有一个角色表t_admin_role
它们之间是有关联的
t_menu:
组件:前端使用vue写的,现在是组件话开发,那么响应的菜单组件呢我们根据后端根据用户id查询到的菜单里面返回包含的comnponent,然后他去生成对应的组件
父id:首先明白菜单不是一级的它是有多级菜单,菜单之间相互关联是通过父id
比如表中id:1 代表所有的菜单
页面上显示的一级菜单23456
id:2 3 4 5 6 parentId对应1
id:7 8 parentId :2说明它是id为2的子菜单
首先Menu类中修改:
我们返回的时候它会存在子菜单,需要注意的是我们现在使用的是MyBatis-plus有对应的注解因为children字段属性在表字段中是不存在的,所以需要通过注解设置一下,让他知道表字段中没有children这个东西,不然的话不设置他去查询的话,它会查children会报错,因为字段里没有
package com.xxxx.server.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
/**
* <p>
*
* </p>
*
* @author zhanglishen
* @since 2022-08-05
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_menu")
@ApiModel(value="Menu对象", description="")
public class Menu implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "url")
private String url;
@ApiModelProperty(value = "path")
private String path;
@ApiModelProperty(value = "组件")
private String component;
@ApiModelProperty(value = "菜单名")
private String name;
@ApiModelProperty(value = "图标")
private String iconCls;
@ApiModelProperty(value = "是否保持激活")
private Boolean keepAlive;
@ApiModelProperty(value = "是否要求权限")
private Boolean requireAuth;
@ApiModelProperty(value = "父id")
private Integer parentId;
@ApiModelProperty(value = "是否启用")
private Boolean enabled;
@ApiModelProperty(value = "子菜单")
@TableField(exist = false) //注解表示表字段中没有
private List<Menu> children;
}
去写我们的接口MenuController:
首先修改@RequestMapping的路径因为在t_menu表里面已经定义好路径了,到时候我们回去做过滤拦截,拦截的时候就是通过url拦截的,菜单一般是放在系统管理里面前缀是:/system/cfg
需要注意一点Controller中的通过id查询菜单列表,Controller并没有传用户id过来,需要注意用户正常登录之后,用户的相关信息都是通过后端直接获取的,而不是通过前端传进来,因为如果前端传进来的话可能会出问题,可能会被人篡改,怎么获取呢,我们现在用的是SprignSecurity他有一个全局对象 ,这个全局对象只要你登录之后它会一直存在,我们可以通过全局对象获取到用户Id
package com.xxxx.server.controller;
import com.xxxx.server.pojo.Menu;
import com.xxxx.server.service.IAdminService;
import com.xxxx.server.service.IMenuService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author zhanglishen
* @since 2022-08-05
*/
@RestController
@RequestMapping("/system/cfg")
public class MenuController {
//注入IMenuService
@Autowired
private IMenuService menuService;
@ApiOperation(value = "通过用户Id查询用户列表")
@GetMapping("/menu")
public List<Menu> getMenusByAdminId(){
return menuService.getMenusByAdminId();
}
}
IMenuService接口:
package com.xxxx.server.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xxxx.server.pojo.Menu;
import java.util.List;
/**
* <p>
* 服务类
* </p>
*
* @author zhanglishen
* @since 2022-08-05
*/
public interface IMenuService extends IService<Menu> {
//根据用户id查询菜单列表
List<Menu> getMenusByAdminId();
}
IMenuServiceImpl :
package com.xxxx.server.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xxxx.server.mapper.MenuMapper;
import com.xxxx.server.pojo.Admin;
import com.xxxx.server.pojo.Menu;
import com.xxxx.server.service.IMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author zhanglishen
* @since 2022-08-05
*/
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService {
@Autowired
private MenuMapper menuMapper;
//根据用户id查询菜单列表
@Override
public List<Menu> getMenusByAdminId() {
return menuMapper.getMenusByAdminId(((Admin)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId());
}
}
AdminMapper:接口
package com.xxxx.server.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xxxx.server.pojo.Menu;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author zhanglishen
* @since 2022-08-05
*/
public interface MenuMapper extends BaseMapper<Menu> {
//根据用户id查询,菜单列表
List<Menu> getMenusByAdminId(Integer id);
}
MenuMapper.xml:写sql语句
sql有两种写法:在我们处理的时候呢,我们先查出所有的一级菜单,然后再 把一级菜单的id传进去去查它的子菜单最后合并起来返回给前端,这样需要查询多次数据库,我们不使用这种写法,我们希望只查询一次数据库,就把菜单一级子菜单都查询出来,菜单和子菜单都在menu表里面,怎么查呢 ,使用自关联就可以了
现在可视化工具里写好sql语句:
SELECT
DISTINCT
m1.*,
m2.id AS id2,
m2.url AS url2,
m2.path AS path2,
m2.component AS component2,
m2.`name` AS `name2`,
m2.iconCls AS iconCls2,
m2.keepAlive AS keepAlive2,
m2.requireAuth AS requireAuth2,
m2.parentId AS parentId2,
m2.enabled AS enabled2
FROM
t_menu m1,
t_menu m2,
t_admin_role ar,
t_menu_role mr
WHERE
m1.id=m2.parentId
AND
m2.id=mr.mid
AND
mr.rid=ar.rid
AND
ar.adminId=1
AND
m2.enabled=TRUE
<?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="com.xxxx.server.mapper.MenuMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.xxxx.server.pojo.Menu">
<id column="id" property="id" />
<result column="url" property="url" />
<result column="path" property="path" />
<result column="component" property="component" />
<result column="name" property="name" />
<result column="iconCls" property="iconCls" />
<result column="keepAlive" property="keepAlive" />
<result column="requireAuth" property="requireAuth" />
<result column="parentId" property="parentId" />
<result column="enabled" property="enabled" />
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, url, path, component, name, iconCls, keepAlive, requireAuth, parentId, enabled
</sql>
<resultMap id="Menus" type="com.xxxx.server.pojo.Menu" extends="BaseResultMap">
<collection property="children" ofType="com.xxxx.server.pojo.Menu">
<id column="id2" property="id" />
<result column="url2" property="url" />
<result column="path2" property="path" />
<result column="component2" property="component" />
<result column="name2" property="name" />
<result column="iconCls2" property="iconCls" />
<result column="keepAlive2" property="keepAlive" />
<result column="requireAuth2" property="requireAuth" />
<result column="parentId2" property="parentId" />
<result column="enabled2" property="enabled" />
</collection>
</resultMap>
<!--根据用户id查询菜单列表-->
<select id="getMenusByAdminId" resultMap="Menus">
SELECT DISTINCT
m1.*,
m2.id AS id2,
m2.url AS url2,
m2.path AS path2,
m2.component AS component2,
m2.`name` AS `name2`,
m2.iconCls AS iconCls2,
m2.keepAlive AS keepAlive2,
m2.requireAuth AS requireAuth2,
m2.parentId AS parentId2,
m2.enabled AS enabled2
FROM
t_menu m1,
t_menu m2,
t_admin_role ar,
t_menu_role mr
WHERE
m1.id=m2.parentId
AND
m2.id=mr.mid
AND
mr.rid=ar.rid
AND
ar.adminId=#{id}
AND
m2.enabled=TRUE
</select>
</mapper>
打开接口文档:
点击发送就查询出来的所有的列表: