SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题

先参考文章https://blog.csdn.net/grd_java/article/details/107584578,了解springSecurity基本操作,另外,数据库也在这篇文章创建了
源码,码云https://gitee.com/yin_zhipeng/spring-security-scaffolding

文章目录

  • 一、环境说明(太基本的就不讲了,本文主要讲如何前后端分离模式整合Security)
  • 二、配置Security
    • 1. 用户实体类继承UserDetails接口
    • 2. 编写JWT工具类
    • 3. 配置JWT过滤器
    • 4. 自定义返回结果
    • 5. Security配置类
  • 三、实现登录逻辑
    • 1. 专门给登录用户的实体类
    • 2. controller
    • 3. service
  • 四、通过swagger测试
    • 1. 配置swagger,携带指定请求头authorization,规定哪些路径需要认证(只有规定的路径才会携带请求头)
    • 2. 测试
  • 五、验证码实现
    • 1. 生成验证码
    • 2. 修改service逻辑
    • 3. 使用redis改造,将验证码存储到redis中
  • 六、实现权限管理系统
  • 七、解决高版本循环依赖和springboot2.6.x与swagger不兼容问题

基本效果说明
  1. 请求login接口,获取token
  2. 获取token后,通过封装名为Authorization的请求头,值为Bearer+空格+token字符串的形式请求需要授权接口(就是请求时,需要带上authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE2Mzk0NjA2MTYsImV4cCI6MTYzOTQ2MTMwMCwic3ViIjoiYWRtaW4iLCJjcmVhdGVkIjoxNjM5NDYwNjE2MTI0fQ._oKG2qarbKi84gxdQjHoRSHd3hx-INQn1CscMgAiASW0B6tsPIjWi1LMr35OTtWR-WvQ8R6tRAvkp0Q3RQm0LQ这样的请求头)
  1. authorization是请求头key
  2. Bearer 是我们的一个头标识
  3. 剩下的就是token字符串

一、环境说明(太基本的就不讲了,本文主要讲如何前后端分离模式整合Security)

common除了Spring security模块外,都在这篇文章中有讲解https://blog.csdn.net/grd_java/article/details/107452826
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第1张图片
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第2张图片
不知道怎么建表,请到文章开头的链接中参考
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第3张图片
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第4张图片

二、配置Security

首先,我们在yml中需要配置一些参数,是我们个人规定好的
  1. 我们规定相应的token 请求头为authorization:Bearer token字符串
    SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第5张图片
jwt:
  tokenHeader: Authorization #JWT存储的请求头,请求是这个就是token的key
  secret: ukc8BDbRigUDaY6pZFfWus2jZWLPHO #JWT加解密使用的密钥
  expiration: 684808 #JWT有效时间(60*60*24)
  tokenHead: Bearer #JWT负载中拿到的开头,token字符串,tokenHead是字符串的头,也就是以它开头

1. 用户实体类继承UserDetails接口

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第6张图片

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
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.util.Collection;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="DdUser对象,使用Spring Security框架就要继承UserDetails接口,实现方法,将返回值改为true", description="")
public class DdUser implements UserDetails {
    private static final long serialVersionUID=1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.ID_WORKER_STR)
    private Integer id;
    private String username;
    private String password;

    /**
     * 所有权限
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    /**
     * 账号是否过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 账号是否被锁定
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 凭证(密码)是否过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否启用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

2. 编写JWT工具类

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第7张图片

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 org.springframework.util.StringUtils;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT工具类
 */
@Component
public class JwtTokenUtil {
    private static final String CLAIM_KEY_USERNAME="sub";//荷载,用户名的key
    private static final String CLAIM_KEY_CREATED="created";//荷载,创建时间key
    /**
     * 项目中推荐使用此形式的密钥和过期时间,因为解耦合
     */
    @Value("${jwt.secret}")
    private String secret;//密钥
    @Value("${jwt.expiration}")
    private Long expiration;//过期时间

    /**
     * 不推荐使用这种,因为写死到了代码中
     */
    public static final long EXPIRE = 1000 * 60 * 60 * 24;                      //设置token过期时间
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";   //设置token密钥,我瞎写的,每个公司都有按自己规则生成的密钥

    //生成token字符串

    /**
     * 根据荷载生成token字符
     * @param id 密言1
     * @param nickname 密言2
     * @return
     */
    public static String getJwtToken(String id, String nickname){

        String JwtToken = Jwts.builder()                                          //构建jwt字符串
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")                             //设置jwt头信息

                .setSubject("guli-user")                                           //分类,名字随便起的,不同的分类可以设置不同的过期
                .setIssuedAt(new Date())                                           //设置过期时间的计时起始值为当前时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))      //设置过期时间为当前时间+EXPIRE我们设定的过期时间

                .claim("id", id)                                                //token主体,这里放你需要的信息,我们实现登陆,就放用户登陆信息
                .claim("nickname", nickname)                                    //需要多少主体信息,就设置多少个claim属性

                .signWith(SignatureAlgorithm.HS256, APP_SECRET)                    //签名哈希,根据指定规则和我们的密钥设定签名
                .compact();

        return JwtToken;
    }

    /**
     * 根据spring security 对象生成token
     * @param userDetails UserDetails对象,spring security会将用户信息保存在这个对象
     * @return
     */
    public String getJwtToken(UserDetails userDetails){

        //分组荷载,也就是主体信息,我们将用户名和有效时间封装到token中
        Map<String,Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED,new Date());

        String JwtToken = gennerateToken(claims);

        return JwtToken;
    }
    private String gennerateToken(Map<String,Object> claims){
        String JwtToken =Jwts.builder()                                          //构建jwt字符串
//                .setHeaderParam("typ", "JWT")
//                .setHeaderParam("alg", "HS256")                             //设置jwt头信息
//
//                .setSubject("dd-user")                                             //分类,名字随便起的,不同的分类可以设置不同的过期
                .setIssuedAt(new Date())                                           //设置过期时间的计时起始值为当前时间
                .setExpiration(new Date(System.currentTimeMillis() + expiration))  //设置过期时间为当前时间+EXPIRE我们设定的过期时间
                .addClaims(claims)
//                .claim(claims)                          //token主体,这里放你需要的信息,我们实现登陆,就放用户登陆信息
//                .claim()                               //需要多少主体信息,就设置多少个claim属性
                .signWith(SignatureAlgorithm.HS512, secret)                    //签名哈希,根据指定规则和我们的密钥设定签名
                .compact();
        return JwtToken;
    }
    /**
     * 从token中获取登录用户名
     */
    public String getUsernameByToken(String token){
        String username;

        try {
            Claims claims = getClaimsFormToken(token); //获取主体内容
            //claims.get(CLAIM_KEY_USERNAME);
            username = claims.getSubject();//获取主体值,上面注释代码和这个都可以,获取的是CLAIM_KEY_USERNAME常量封装的key,对应的值
        }catch (Exception e){
            username = null;
            e.printStackTrace();
        }
        return username;

    }

    /**
     * 从token中获取荷载
     * @param token
     * @return
     */
    private Claims getClaimsFormToken(String token){
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();//解析token字符串,获取主体内容
        }catch (Exception e){
            e.printStackTrace();
        }
        return claims;
    }

    /**
     * 从荷载中获取过期时间
     */
    private Date getExpiredDateFromToken(String token){
        Claims claimsFormToken = getClaimsFormToken(token);
        return claimsFormToken.getExpiration();
    }
    /**
     * 验证token是否失效,失效返回true,没失效返回false
     */
    private boolean isTokenExpired(String token){
        Date expiredDateFromToken = getExpiredDateFromToken(token);
//        boolean before = expiredDateFromToken.before(new Date());
        return expiredDateFromToken.before(new Date());//如果当前时间再过期时间之前,就是还没有到过期时间,返回true,否则返回false
    }

    /**
     * 判断token是否有效
     * @param jwtToken token字符串
     * @return
     */
    public boolean checkToken(String jwtToken,UserDetails userDetails) {
        if(StringUtils.isEmpty(jwtToken)) return false;//如果为空直接返回false表示失效
        String username;
        try {
             username = getUsernameByToken(jwtToken);//根据token获取用户名
        } catch (Exception e) {
            e.printStackTrace();
            username = null;
            return false;//获取不到用户名,直接失效
        }
        if(!username.equals(userDetails.getUsername())) return false;//如果token的用户名与当前UserDetails用户名不一致,直接失效
        if(isTokenExpired(jwtToken)) return false;//如果token失效,返回false
        return true;
    }
    /**
     * 判断token是否可以被刷新
     */
    public boolean canRefresh(String token){
        return !isTokenExpired(token);//已经过期,表示可以被刷新
    }

    /**
     * 刷新token
     */
    public String refreshToken(String token){
        Claims claimsFormToken = getClaimsFormToken(token);
        claimsFormToken.put(CLAIM_KEY_CREATED,new Date());
        return gennerateToken(claimsFormToken);
    }
}

3. 配置JWT过滤器

任何请求都需要进行过滤,如果请求携带我们规定的token,那么将token放在spring Security对象中

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第8张图片

import com.dd.security.utils.JwtTokenUtil;
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;
import java.util.Enumeration;

/**
 * JWT登录授权过滤器,继承OncePerRequestFilter
 */
public class JwtAuthencationTokenFilter extends OncePerRequestFilter {


    @Value("${jwt.tokenHeader}")
    private String tokenHeader;//JWT存储的请求头

    @Value("${jwt.tokenHead}")
    private String tokenHead;//JWT负载中拿到的开头,token字符串,tokenHead是字符串的头,也就是以它开头
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 前置拦截
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
//        Enumeration headerNames = request.getHeaderNames();//debug用的
        //通过tokenHeader获取验证头,也就是tokenHead+token字符串
        String authHeader = request.getHeader(tokenHeader);

        //如果验证头存在,并且token字符串符合我们规定的tokenHead开头
        if(authHeader!=null && authHeader.startsWith(tokenHead)){
            //将token截取出来
            String authToken = authHeader.substring(tokenHead.length());
            //根据token获取用户名
            String username = jwtTokenUtil.getUsernameByToken(authToken);
            //如果token存在,但是Security全局上下文没有用户,表示用户没有登录
            if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
                //登录
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                //如果token有效
                if(jwtTokenUtil.checkToken(authToken,userDetails)){
                    //更新登录用户对象UserDetails
                    UsernamePasswordAuthenticationToken authenticationToken =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    //重新设置到用户对象中
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    //设置到security全局上下文
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }

        filterChain.doFilter(request,response);//放行
    }
}

4. 自定义返回结果

当访问接口没有权限,自定义返回结果

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第9张图片

import com.dd.common_utils.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
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 RestAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");//设置响应编码
        response.setContentType("application/json");//设置响应格式,前后端分离最好是json
        PrintWriter out = response.getWriter();//获取response输出流
        //创建返回结果对象
        Result result = Result.error().code(403).message("权限不足,请联系管理员");
        //以json字符串形式放到response输出流中
        out.write(new ObjectMapper().writeValueAsString(result));
        out.flush();//刷新
        out.close();//关闭流
    }
}
当未登录或token失效时访问接口,自定义返回结果

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第10张图片

import com.dd.common_utils.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
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");//设置响应格式,前后端分离最好是json
        PrintWriter out = response.getWriter();//获取response输出流
        //创建返回结果对象
        Result result = Result.error().code(401).message("请登录!!!");
        //以json字符串形式放到response输出流中
        out.write(new ObjectMapper().writeValueAsString(result));
        out.flush();//刷新
        out.close();//关闭流
    }
}

5. Security配置类

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第11张图片
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第12张图片
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第13张图片
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第14张图片

import com.dd.security.entity.DdUser;
import com.dd.security.exception.RestAccessDeniedHandler;
import com.dd.security.exception.RestAuthorizationEntryPoint;
import com.dd.security.filter.JwtAuthencationTokenFilter;
import com.dd.security.service.DdUserService;
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;


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DdUserService ddUserService;

    @Autowired
    private RestAccessDeniedHandler restAccessDeniedHandler;//当访问接口没有权限,自定义返回结果
    @Autowired
    private RestAuthorizationEntryPoint restAuthorizationEntryPoint;//当未登录或token失效时访问接口,自定义返回结果
    /**
     * 配置密码解析Bean实例
     * 加载到IOC容器
     * 这是Spring Security 自定义登录逻辑的硬性要求
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置UserDetailsService
     */
    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        return username->{
            DdUser user = ddUserService.getLoginInfoByUsername(username);
            if(user == null){
                return null;
            }
            return user;
        };
    }

    /**
     * 配置JWT登录授权过滤器
     */
    @Bean
    public JwtAuthencationTokenFilter jwtAuthencationTokenFilter(){
        return new JwtAuthencationTokenFilter();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService())//设置Security使用的userDetailsService,为我们上面配置的
                .passwordEncoder(passwordEncoder());//设置使用的passwordEncoder为我们上面配置的passwordEncoder
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //使用JWT不需要csrg
        http.csrf().disable()
                //使用Token,不需要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()//接下来配置授权
                .authorizeRequests()
                //下面授权在configure(WebSecurity web)方法中配置了,这里不需要了
//                .antMatchers("/login","/logout").permitAll()//允许访问/login,/logout的请求无需认证即可通行
                .anyRequest().authenticated()//除了上面配置的,剩下的请求全部拦截,必须认证通过才能访问
                .and()//接下来配置缓存
                .headers()
                .cacheControl()
                ;
        //添加JWT登录授权过滤拦截器
        http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //添加自定义未授权,和未登录结果返回,前后端分离,需要返回状态码
        http.exceptionHandling()
                .accessDeniedHandler(restAccessDeniedHandler)
                .authenticationEntryPoint(restAuthorizationEntryPoint);

    }

    /**
     * 放行路径配置
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers("/login",
                        "/logout",
                        "/css/**",
                        "/js/**",
                        "/swagger-ui.html",
                        "/webjars/**",
                        "/swagger-resources/**",
                        "/v2/api-docs/**"
                );
    }
}

三、实现登录逻辑

1. 专门给登录用户的实体类

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第15张图片

2. controller

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第16张图片

import com.dd.common_utils.Result;
import com.dd.security.entity.DdUser;
import com.dd.security.entity.UserLogin;
import com.dd.security.service.DdUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
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;

@RestController
@Api(tags = "LoginController,登录")
public class LoginController {

    @Autowired
    private DdUserService ddUserService;

    @ApiOperation(value = "登录之后返回token,如果想要带着token请求,需要添加Bearer前缀,Bearer token,中间空格分隔")
    @PostMapping("/login")
    public Result login(@RequestBody UserLogin userLogin, HttpServletRequest request){
        if(userLogin == null){
            Result.error().message("请输入用户名和密码");
        }
        if(userLogin.getUsername()==null){
            Result.error().message("请输入用户名");
        }
        if(userLogin.getPassword()==null){
            Result.error().message("请输入密码");
        }
        return ddUserService.login(userLogin,request);
    }

    @ApiOperation(value = "获取当前登录用户信息")
    @GetMapping("/login/info")
    public Result loginInfo(@ApiParam(value ="全局对象,security将信息设置到全局,通过这个就可以获取")
                            Principal principal){
        if(principal == null){
            return Result.error().message("用户信息不存在");
        }
        String username = principal.getName();//从全局中获取username
        //根据用户名获取用户对象
        DdUser ddUser =  ddUserService.getLoginInfoByUsername(username);
        if(ddUser == null){
            return Result.error().message("用户信息不存在");
        }
        ddUser.setPassword(null);//无论如何都不可以返回密码
        return Result.ok().message("用户信息获取成功").data("loginInfo",ddUser);
    }

    @ApiOperation(value = "退出登录")
    @PostMapping("/logout")
    public Result logout(){
        //这里省略了退出登录逻辑,比如销毁token,销毁数据库,redis中缓存等,因为没有引入redis,所以直接返回退出成功
        return Result.ok().message("退出登录");
    }
}

3. service

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第17张图片
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第18张图片

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dd.common_utils.Result;
import com.dd.security.entity.DdUser;

import com.dd.security.entity.UserLogin;
import com.dd.security.mapper.DdUserMapper;
import com.dd.security.service.DdUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dd.security.utils.JwtTokenUtil;
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;
@Service
public class DdUserServiceImpl extends ServiceImpl<DdUserMapper, DdUser> implements DdUserService {

    @Autowired
    private DdUserMapper ddUserMapper;

    @Autowired
    private UserDetailsService userDetailsService;//spring security UserDetailsService,需要我们自己实现它

    @Autowired
    private PasswordEncoder passwordEncoder;//PasswordEncoder,需要我们自己配置出来

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.tokenHead}")
    private String tokenHead;
    /**
     * 登录之后返回token
     */
    @Override
    public Result login(UserLogin userLogin, HttpServletRequest request) {
        //获取userDetails对象,这里我们userDetailsServiceImpl实现的是,它通过用户名查询了数据库的密码
        UserDetails userDetails = userDetailsService.loadUserByUsername(userLogin.getUsername());
        //如果用户为空,
        if(userDetails==null){
            return Result.error().message("用户不存在");
        }
        //密码匹配失败
        if(!passwordEncoder.matches(userLogin.getPassword(),userDetails.getPassword())){
            return Result.error().message("密码错误");
        }
        //下面这个因为我们DdUser将isEnabled写死为true,所以就不写这些判断了,其实都应该写成属性,存储到数据库
//        if(!userDetails.isEnabled()){
//            return Result.error().message("用户被禁言,请联系管理员");
//        }

        //更新登录用户对象UserDetails 如果获取对象成功,没有错误,那么使用Spring Security我们需要将对象设置到UserDetails中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);//将其放在Security全局中
        //生成token,返回
        String jwtToken = jwtTokenUtil.getJwtToken(userDetails);
        return Result.ok().message("登录成功,获取token")
                .data("token",jwtToken)//token字符串
                .data("tokenHead",tokenHead);//tokenheader,key
    }

    /**
     * 根据用户名获取登录对象
     * @param username
     * @return
     */
    @Override
    public DdUser getLoginInfoByUsername(String username) {
        DdUser ddUser = ddUserMapper.selectOne(new QueryWrapper<DdUser>().eq("username", username));
        if(ddUser == null){
            return null;
        }
        return ddUser;
    }
}

四、通过swagger测试

1. 配置swagger,携带指定请求头authorization,规定哪些路径需要认证(只有规定的路径才会携带请求头)

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第19张图片
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第20张图片
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第21张图片

package com.dd.service_base.config;

import com.google.common.base.Predicates;
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.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.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;

@Configuration
@EnableSwagger2//表中此类为Swagger2
public class SwaggerConfig {
    @Bean
    public Docket webApiConfig() {
        List<Parameter> pars = new ArrayList<Parameter>();
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("webApi")//分组名
                .apiInfo(webApiInfo())//在线文档的信息,传入ApiInfo对象,就是下面内个方法返回的对象
                .select()
                .paths(Predicates.not(PathSelectors.regex("/admin/.*")))//路径中包含admin时不显示信息
                .paths(Predicates.not(PathSelectors.regex("/error/.*")))
                .build()
                //因为我们开启了securityJWT验证,所以我们也需要给Swagger配置头信息
                .securityContexts(securityContexts())
                .securitySchemes(securitySchemes())
                ;
    }

    private ApiInfo webApiInfo() {
        return new ApiInfoBuilder()
                .title("gulischool 接口 API 文档")
                .description("展示先做基础功能,后面再添加业务")
                .termsOfServiceUrl("https://www.dd.com/aa/")
                .version("1.0")
                .contact(new Contact("Helen","http://dd.com","[email protected]"))
                .build();
    }


    private List<ApiKey> securitySchemes(){
        //设置头信息
        ArrayList<ApiKey> result = new ArrayList<>();
        ApiKey apiKey = new ApiKey("Authorization", "Authorization", "Header");
        result.add(apiKey);
        return result;
    }

    /**
     * 设置哪些路径需要授权
     * @return
     */
    private List<SecurityContext> securityContexts(){
        //设置需要授权认证的路径
        ArrayList<SecurityContext> result = new ArrayList<>();
        result.add(getContextByPath("/service_animation/.*"));
//        result.add(getContextByPath("/login/.*"));
        result.add(getContextByPath("/login/info"));
        result.add(getContextByPath("/logout"));
        result.add(getContextByPath("/security/*"));
        return result;
    }

    private SecurityContext getContextByPath(String pathRegex) {
        return SecurityContext.builder()
                .securityReferences(defaultAuth())
//                .forPaths(PathSelectors.regex(pathRegex))
                .forPaths(PathSelectors.regex("^(?!auth).*$"))
                .build();
    }

    private List<SecurityReference> defaultAuth() {
        ArrayList<SecurityReference> result = new ArrayList<>();
        //设置授权范围为全局global
        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");

        //官方要求必须放在数组中,所以将其放在数组中
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0]=authorizationScope;
        //官方要求List中添加new SecurityReference("Authorization",authorizationScopes)
        result.add(new SecurityReference("Authorization",authorizationScopes));
        return result;
    }
}

2. 测试

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第22张图片
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第23张图片
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第24张图片
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第25张图片

前端呢,只需要登录后按规则保存token,请求时,携带这个token就可以访问了

五、验证码实现

更改实体类,我们登录时需要传入验证码了

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第26张图片

引入依赖

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第27张图片

1. 生成验证码

验证码配置类

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第28张图片

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;
/**
 * 验证码配置类
 */
@Configuration
public class CaptchaConfig {
    @Bean
    public DefaultKaptcha defaultKaptcha(){
        //验证码生成器
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        //配置
        Properties properties = new Properties();
        //时候有边框
        properties.setProperty("kaptcha.border","yes");
        //设置边框颜色
        properties.setProperty("kaptcha.border.color","105,179,98");
        //验证码
        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");
        //验证码图片宽度,默认208
        properties.setProperty("kaptcha.image.width","100");
        //验证码图片高度,默认40
        properties.setProperty("kaptcha.image.height","40");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}
controller

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第29张图片

import com.google.code.kaptcha.impl.DefaultKaptcha;
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 {
    @Autowired
    private DefaultKaptcha defaultKaptcha;

    @ApiOperation(value = "验证码,以image/jpeg格式固定响应")
    @GetMapping(value = "/captcha",produces = "image/jpeg")
    public void captcha(HttpServletRequest request, HttpServletResponse response)  {

        /**定义response输出类型为image/jpeg类型**/
        response.setDateHeader("Expires",0);
        // Set standard HTTP/1.1 no-cache headers.
        response.setHeader("Cache-Control","no-store, no-cache, must-revalidate");
        // Set IE extended HTTP/1.1 no-cache headers(use addHeader)
        response.addHeader("Cache-Control","post-check=0,pre-check=0");
        // Set standard HTTP/1.0 no-cache header.
        response.setHeader("Pragma","no-cache");
        // return a jpeg
        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();
                }
            }
        }
    }
}
swagger测试

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第30张图片
SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第31张图片

2. 修改service逻辑

SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第32张图片

        //从session中获取验证码
        String captcha = (String) request.getSession().getAttribute("captcha");
        //如果验证码为空,或者验证码和session中验证码不一致(忽略大小写比较),返回错误
        if(StringUtils.isEmpty(captcha)||!captcha.equalsIgnoreCase(userLogin.getCode())){
            return Result.error().message("验证码错误!!!");
        }

3. 使用redis改造,将验证码存储到redis中

为了节省篇幅,将redis的改造放在下面实现权限管理系统的文章中

六、实现权限管理系统

因为篇幅限制,我将其放在这篇文章中https://blog.csdn.net/grd_java/article/details/121932440

七、解决高版本循环依赖和springboot2.6.x与swagger不兼容问题

实现了权限管理系统后,出现高版本循环依赖的问题,如下
  1. 当spring boot版本为2.6.X以上时,出现问题
    SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第33张图片
    SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第34张图片
原因如下
  1. SecurityConfig配置类中,注入了private DdUserService ddUserService;
    SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第35张图片
  2. DdUserServiceImpl又注入了UserDetailsService
    SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第36张图片
解决方案
  1. SecurityConfig注释private DdUserService ddUserService;,然后使用DdUserMapper
    SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第37张图片
    SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第38张图片
循环依赖解决,但是又出现了最近经常出现的问题,springboot2.6.x与swagger2不兼容的问题,这个和咱代码就没关系了,解决方案如下,修改配置,或者升级swagger或者降低springboot版本到2.5.x
  1. 解决循环依赖,新问题如下,表面上是swagger2找不到东西了,其实是ant_path_matcher参数的问题
    SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第39张图片
  2. 配置yaml参数
    SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第40张图片
spring:
  main:
    allow-circular-references: true # 关闭spring boot 循环注入检测
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher # 解决spring boot 高版本不兼容swagger2.x版本问题
  1. 问题解决,项目跑起来了,但是此时swagger不能用了,显示跨域问题
    SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第41张图片
  2. 修改跨域配置
    SpringSecurity前后端分离,登录逻辑,基于SpringSecurity实现权限管理系统,解决高版本循环依赖和springboot2.6.x与swagger不兼容问题_第42张图片
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
//                .allowedOrigins("*")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }

你可能感兴趣的:(java框架,java,安全,开发语言,springSecurity)