Cloud E随笔-后端_piece3--实现登录功能

目录

  • 一、spring security 简介
    • 1. 概要
    • 2. Spring Security 的核心功能
    • 3. Spring Security特点
  • 二、 登录功能实现
    • 1. 添加pom依赖
    • 2. 修改配置文件 -application.yml
    • 3. 创建工具类
      • 3.1 新建JWTToken工具类
      • 3.2 Admin实现UserDetails类
      • 3.3 添加公共返回对象
      • 3.4 添加登录相关对象AdminLoginParam
    • 4. 登录功能实现
      • 4.1 LoginController编写
      • 4.2 IAdminService接口编写
      • 4.3 AdminServerImpl接口类的实现
    • 5. Security配置
      • 5.1 准备配置类
      • 5.2 添加jwt 登录授权拦截器
      • 5.3 添加自定义未授权未登录结果返回
  • 三、 接口文档Swagger2准备
    • 1. 依赖添加
    • 2. Swagger2配置
    • 3. 登录验证
    • 4. 添加验证码模块
      • 4.1 添加验证码依赖
      • 4.2 添加验证码配置文件
      • 4.3 验证码生成
      • 4.4 修改登录传递参数
    • 5. 登录成功

一、spring security 简介

1. 概要

Spring Security是Spring家族中的一员,Security基于Spring框架,提供了一套Web应用安全性的完整解决方案。

2. Spring Security 的核心功能

主要包括:

  • 认证 (你是谁)
  • 授权 (你能干什么)
  • 攻击防护 (防止伪造身份)

其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。

3. Spring Security特点

  • 和 Spring 无缝整合。
  • 全面的权限控制。
  • 专门为 Web 开发而设计。
    1. 旧版本不能脱离 Web 环境使用。
    2. 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独引入核心模块就可以脱离 Web 环境。
  • 重量级(缺点)。

二、 登录功能实现

项目中使用 Spring Security 框架实现登录功能

1. 添加pom依赖


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-securityartifactId>
dependency>

<dependency>
    <groupId>io.jsonwebtokengroupId>
    <artifactId>jjwtartifactId>
    <version>0.9.1version>
dependency>

2. 修改配置文件 -application.yml

# jwt配置
jwt:
  # JWT存储的请求头
  tokenHeader: Authorization
  # JWT 加解密使用的密钥
  secret: cloude-secret
  # JWT的超期限时间(60*60*24)
  expiration: 604800
  # JWT 负载中拿到开头
  tokenHead: Bearer

3. 创建工具类

3.1 新建JWTToken工具类

创建config.security目录,并且在其目录新建JwtTokenUtil.java文件

package com.chuci.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;

/**
 * @Auther chuci
 * @Data 2021-12-22 22:43
 * @Description:
 */
@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;     //JWT密钥

    @Value("${jwt.expiration}")
    private Long expiration;    //JWT失效时间


    /**
     * 根据用户信息生成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);
    }

    /**
     * 从token中获取用户名
     * @param token
     * @return
     */
    public String getUserNameFromToken(String token){
        String username;
        try {
            Claims claims = getClaimsFromToken(token);  //根据token获取荷载
            username = claims.getSubject();
        }catch (Exception e){
            username = null;
            e.printStackTrace();
        }
        return username;
    }


    /**
     * 判断token是否有效
     * @param token
     * @param userDetails
     * @return
     */
    public boolean validateToken(String token, UserDetails userDetails){
        String username = getUserNameFromToken(token);
//        判断username是否一致以及token是否失效
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }


    /**
     * 验证token是否可以被刷新
     * @param token
     * @return
     */
    public boolean canRefresh(String token){
        return !isTokenExpired(token);   //token过期就可以被刷新了
    }


    /**
     * 刷新token
     * @param token
     * @return
     */
    public String refreshToken(String token){
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());  //更新创建时间 达到刷新token的目的
        return generateToken(claims);
    }




    /**
     * 判断token是否失效
     * @param token
     * @return
     */
    private boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }


    /**
     * 从token中获取时间
     * @param token
     * @return
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }


    /**
     * 从token中获取荷载
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()     //转荷载
                    .setSigningKey(secret)  //添加签名
                    .parseClaimsJws(token)  //添加密钥
                    .getBody();  //拿到荷载
        }catch (Exception e){
            e.printStackTrace();
        }
        return claims;
    }


    /**
     * 根据JWT生成token 私有 只需public String generateToken调用
     * @param claims
     * @return
     */
    private String generateToken(Map<String, Object> claims){
        return Jwts.builder()  //jwts生成
                .setClaims(claims)  //荷载
                .setExpiration(generateExporation())  //失效时间
                .signWith(SignatureAlgorithm.HS512, secret)  //签名
                .compact();  //密钥
    }


    /**
     * 生成token失效时间
     * @return
     */
    private Date generateExporation() {
//        系统当前时间 加失效时间
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }
}

3.2 Admin实现UserDetails类

Cloud E随笔-后端_piece3--实现登录功能_第1张图片
实现UserDetails,重写其方法,将所有返回类型改为 true,但注意isEnabled()方法是否启用账号,返回值返回Admin类中enable属性值,因此isEnabled()方法返回值为enabled
在这里插入图片描述
Cloud E随笔-后端_piece3--实现登录功能_第2张图片

package com.chuci.server.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Collection;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

/**
 * 

* *

* * @author chuci * @since 2021-12-22 */
@TableName("t_admin") @ApiModel(value = "Admin对象", description = "") public class Admin implements Serializable, UserDetails { private static final long serialVersionUID = 1L; @ApiModelProperty("id") @TableId(value = "id", type = IdType.AUTO) private Integer id; @ApiModelProperty("姓名") private String name; @ApiModelProperty("手机号码") private String phone; @ApiModelProperty("住宅电话") private String telephone; @ApiModelProperty("联系地址") private String address; @ApiModelProperty("是否启用") private Boolean enabled; @ApiModelProperty("用户名") private String username; @ApiModelProperty("密码") private String password; @ApiModelProperty("用户头像") private String userFace; @ApiModelProperty("备注") private String remark; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getTelephone() { return telephone; } public void setTelephone(String telephone) { this.telephone = telephone; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public Boolean getEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public String getUsername() { return username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getUserFace() { return userFace; } public void setUserFace(String userFace) { this.userFace = userFace; } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark; } /** * 重写 实现 UserDetails * @return */ @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; //是否启用 } public void setUsername(String username) { this.username = username; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } /** * 重写toString * @return */ @Override public String toString() { return "Admin{" + "id=" + id + ", name=" + name + ", phone=" + phone + ", telephone=" + telephone + ", address=" + address + ", enabled=" + enabled + ", username=" + username + ", password=" + password + ", userFace=" + userFace + ", remark=" + remark + "}"; } }

3.3 添加公共返回对象

每次请求,为了前后端参数统一,规定公共返回对象SysResult
创建vo目录,并且新建SysResult.java。包含状态码code, 返回信息message以及返回对象data。并创建success方法以及error方法。同是使用lombok生成无参全参构造方法以及get/set方法。
注:返回对象可返回任何对象类型。

package com.chuci.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 公共返回对象
 *
 * @Auther chuci
 * @Data 2022-01-09 22:26
 * @Description:
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysResult {

    private long code;              //状态码
    private String message;         //返回信息
    private Object data;            //返回对象


    /**
     * 请求成功返回对象
     * @return
     */
    public static SysResult success(){
        return new SysResult(200, "服务请求成功", null);
    }

    public static SysResult success(String msg){
        return new SysResult(200, msg, null);
    }

    public static SysResult success(String msg, Object data){
        return new SysResult(200, msg, data);
    }


    /**
     * 请求失败
     * @return
     */
    public static SysResult error(){
        return new SysResult(500, "服务请求失败", null);
    }

    public static SysResult error(String msg){
        return new SysResult(500, msg, null);
    }

    public static SysResult error(String msg, Object data){
        return new SysResult(500, msg, data);
    }
}

3.4 添加登录相关对象AdminLoginParam

创建目录结构bean,并且新建文件AdminLoginParam.java文件用来登录时参数传递。若使用Admin实体进行登录实体对象,则传递的对象太大,所以在这里进行简化,暂时仅传递 用户名以及密码即可(后期还需要传递验证码)。

package com.chuci.server.bean;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 用户登录实体类
 *
 * @Auther chuci
 * @Data 2022-01-10 15:08
 * @Description:
 */

@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "AdminLogin实体类", description = "")
public class AdminLoginParam {

    @ApiModelProperty(value = "用户名", required = true)
    private String username;

    @ApiModelProperty(value = "密码", required = true)
    private String password;

}

4. 登录功能实现

4.1 LoginController编写

新建文件LoginController.java并进行登录代码控制层的编写,主要实现:

  1. 用户登录
  2. 登录用户信息获取
  3. 退出登录
package com.chuci.server.controller;

import com.chuci.server.entity.Admin;
import com.chuci.server.service.IAdminService;
import com.chuci.server.bean.AdminLoginParam;
import com.chuci.server.vo.SysResult;
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;

/**
 * 登录
 *
 * @Auther chuci
 * @Data 2022-01-10 15:12
 * @Description:
 */
@Api(tags = "LoginController")
@RestController
public class LoginController {

    @Autowired
    private IAdminService adminService;

    /**
     * 进行登录操作,返回SysResult对象。data包含登录所需验证
     * @param adminLoginParam
     * @param request
     * @return
     */
    @ApiOperation(value = "登陆之后返回token")
    @PostMapping("/login")
    public SysResult login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request){
        return adminService.login(adminLoginParam.getUsername(), adminLoginParam.getPassword(), adminLoginParam.getCode(), request);
    }


    /**
     * 登录成功,获取当前登录对象所有信息
     * @param principal
     * @return
     */
    @ApiOperation(value = "获取当前登录用户信息")
    @GetMapping("/admin/info")
    public Admin getAdminInfo(Principal principal){
        if (principal == null){
            return null;
        }
        String username = principal.getName();
        Admin admin = adminService.getAdminByUserName(username);
        admin.setPassword(null);
        return admin;
    }


    /**
     * 退出登录,前端删除token进行退出操作
     * @return
     */
    @ApiOperation(value = "退出登录")
    @PostMapping("/logout")
    public SysResult logout(){
        return SysResult.success("注销成功");
    }

}

4.2 IAdminService接口编写

由MybatisPlus代码生成器生成的所有Service接口,前缀均有“I”标注,即Admin实体的Service接口为IAdminService。
主要实现登录以及登录用户信息的查询。

public interface IAdminService extends IService<Admin> {

    /**
     * 登录之后返回token
     *
     * @param code
     * @param username
     * @param password
     * @param request
     * @return
     */
    SysResult login(String username, String password, HttpServletRequest request);

    /**
     * 根据用户名获取用户信息
     * @param username
     * @return
     */
    Admin getAdminByUserName(String username);
}

4.3 AdminServerImpl接口类的实现

package com.chuci.server.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.chuci.server.config.security.JwtTokenUtil;
import com.chuci.server.entity.Admin;
import com.chuci.server.mapper.AdminMapper;
import com.chuci.server.service.IAdminService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chuci.server.vo.SysResult;
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;

/**
 * 

* 服务实现类 *

* * @author chuci * @since 2021-12-22 */
@Service public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService { @Autowired private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private AdminMapper adminMapper; @Value("${jwt.tokenHead}") private String tokenHead; /** * 登陆之后返回token * * @param code * @param username * @param password * @param request * @return */ @Override public SysResult login(String username, String password, String code, HttpServletRequest request) { // 登录 UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (userDetails == null || passwordEncoder.matches(password,userDetails.getPassword())){ return SysResult.error("用户密码不正确"); } if(!userDetails.isEnabled()){ return SysResult.error("账号禁用,请联系管理员!"); } // 更新登录用户对象 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 SysResult.success("登录成功", tokenMap); } /** * 根据用户名获取用户信息 * @param username * @return */ @Override public Admin getAdminByUserName(String username) { return adminMapper.selectOne( new QueryWrapper<Admin>() .eq("username", username) .eq("enabled", true)); } }

5. Security配置

5.1 准备配置类

新建文件 SecurityConfig.java
SpringSecurity的登录逻辑是通过UserDetailsService中的loadByUserName来实现的。重写userDetailsService()方法并重新实现configure(AuthenticationManagerBuilder auth)。configure(WebSecurity web)开放部分接口以及资源。configure(HttpSecurity http)使用JWT, 不需要csrf。

package com.chuci.server.config.security;

import com.chuci.server.entity.Admin;
import com.chuci.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;

/**
 * @Auther chuci
 * @Data 2022-01-10 22:34
 * @Description: Security配置类
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private IAdminService adminService;

    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;

    @Autowired
    private RestAuthorizationEntryPoint restAuthorizationEntryPoint;

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


    /**
     * 开放以下接口网页资源不进行安全认证
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/login",
                "/logout",
                "/captcha",
                "css/**",
                "js/**",
                "/webjars/**",
                "/swagger-resources/**",
                "/v2/api-docs/**",
                "/index.html",
                "/doc.html",
                "favicon.ico"

        );
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        使用JWT, 不需要csrf
        http.csrf()
                .disable()
//                基于token, 不需要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
//               所有请求都需要认证
                .anyRequest()
                .authenticated()
                .and()
                .headers()
                .cacheControl();
//        添加jwt 登录授权拦截器
        http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//        添加自定义未授权未登录结果返回
        http.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthorizationEntryPoint);
    }

    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        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();
    }
}

5.2 添加jwt 登录授权拦截器

新建JWTAuthencationTokenFilter.java 文件

package com.chuci.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.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;

/**
 * @Auther chuci
 * @Data 2022-01-10 22:59
 * @Description: 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
        if(authHeader != null && authHeader.startsWith(tokenHead)){
            String authToken = authHeader.substring(tokenHead.length());
            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(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);

                }
            }
        }
        filterChain.doFilter(request, response);
    }
}

5.3 添加自定义未授权未登录结果返回

  1. 未授权结果返回
package com.chuci.server.config.security;

import com.chuci.server.vo.SysResult;
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;

/**
 * @Auther chuci
 * @Data 2022-01-14 22:06
 * @Description:
 */

@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        PrintWriter writer = httpServletResponse.getWriter();
        SysResult result = SysResult.error("权限不足,请联系管理员");
        result.setCode(403);
        writer.write(new ObjectMapper().writeValueAsString(result));
        writer.flush();
        writer.close();
    }
}
  1. 未登录结果返回
package com.chuci.server.config.security;

import com.chuci.server.vo.SysResult;
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;

/**
 * @Auther chuci
 * @Data 2022-01-14 21:42
 * @Description:
 */
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("UTF-8");  //设置编码格式
        httpServletResponse.setContentType("application/json"); //设置传输为JSON格式
        PrintWriter writer = httpServletResponse.getWriter();
        SysResult result = SysResult.error("尚未登录,请登录后再试!");
        result.setCode(401);
        writer.write(new ObjectMapper().writeValueAsString(result));
        writer.flush();
        writer.close();
    }
}

三、 接口文档Swagger2准备

在团队开发中,一个好的 API 文档不但可以减少大量的沟通成本,还可以帮助一位新人快速上手业务。传统的做法是由开发人员创建一份 RESTful API 文档来记录所有的接口细节,并在程序员之间代代相传。

这种做法存在以下几个问题:

  • API 接口众多,细节复杂,需要考虑不同的HTTP请求类型、HTTP头部信息、HTTP请求内容等,想要高质量的完成这份文档需要耗费大量的精力;

  • 难以维护。随着需求的变更和项目的优化、推进,接口的细节在不断地演变,接口描述文档也需要同步修订,可是文档和代码处于两个不同的媒介,除非有严格的管理机制,否则很容易出现文档、接口不一致的情况

Swagger2 的出现就是为了从根本上解决上述问题。它作为一个规范和完整的框架,可以用于生成、描述、调用和可视化 RESTful 风格的 Web 服务:

  1. 接口文档在线自动生成,文档随接口变动实时更新,节省维护成本

  2. 支持在线接口测试,不依赖第三方工具

1. 依赖添加

Swagger2自带UI不太好看,之后更换第三方UI界面


        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger2artifactId>
            <version>2.9.2version>
        dependency>
        






        
        <dependency>
            <groupId>com.google.guavagroupId>
            <artifactId>guavaartifactId>
            <version>31.0.1-jreversion>
        dependency>
        <dependency>
            <groupId>com.github.xiaoymingroupId>
            <artifactId>knife4j-micro-spring-boot-starterartifactId>
            <version>2.0.5version>
        dependency>
        <dependency>
            <groupId>com.github.xiaoymingroupId>
            <artifactId>knife4j-spring-boot-starterartifactId>
            <version>2.0.5version>
        dependency>

2. Swagger2配置

在config文件夹下创建新文件夹swagger存放Swagger2配置类。新建SwaggerConfig.java文件
Cloud E随笔-后端_piece3--实现登录功能_第3张图片
在这里进行简单常用配置

package com.chuci.server.config.swagger;

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.checkerframework.checker.units.qual.A;
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;

/**
 * @Auther chuci
 * @Data 2022-01-14 22:22
 * @Description:
 */
@Configuration
@EnableSwagger2     //启用Swagger2
@EnableKnife4j      //启用Knife4j
public class SwaggerConfig {

    /**
     * 通过@Configuration注解,让Spring来加载该类配置。
     * 再通过@EnableSwagger2注解来启用Swagger2。
     * 

* 再通过createRestApi()函数创建Docket的Bean之后, * apiInfo()用来创建该Api的基本信息(这些基本信息会展现在文档页面中)。 * select()函数返回一个ApiSelectorBuilder实例用来控制哪些接口暴露给Swagger来展现 * 采用扫描所有定义,Swagger会扫描所有Controller定义的API,并产生文档内容(除了被@ApiIgnore指定的请求)。 * * @return */ @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) //标明文档类型 Swagger2 .apiInfo(apiInfo()) //apiInfo()用来创建Api的基本信息(这些信息会展现在文档页面中) .groupName("CloudE_Server") //组名 .select() //select()函数返回一个ApiSelectorBuilder实例用来控制哪些接口暴露给Swagger来展现 .apis(RequestHandlerSelectors.basePackage("com.chuci.server.controller")) //扫描特定包下面的文件 还有.any 扫描所有包 .paths(PathSelectors.any()) //Swagger会扫描该包下的所有Controller定义的API,并产生文档内容(除了被@ApiIgnore定义的请求) .build() .securityContexts(securityContexts()) .securitySchemes(securitySchemes()); } private List<? extends SecurityScheme> securitySchemes() { // 设置请求头信息 List<ApiKey> res = new ArrayList<>(); ApiKey apiKey = new ApiKey("Authorization", "Authorization", "Header"); res.add(apiKey); return res; } private List<SecurityContext> securityContexts() { // 设置需要认证的路径 List<SecurityContext> res = new ArrayList<>(); res.add(getContextByPath("/hello/.*")); return res; } private SecurityContext getContextByPath(String pathRegex) { return SecurityContext.builder() .securityReferences(defaultAuth()) .forPaths(PathSelectors.regex(pathRegex)) .build(); } private List<SecurityReference> defaultAuth() { List<SecurityReference> res = new ArrayList<>(); AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; authorizationScopes[0] = authorizationScope; res.add(new SecurityReference("Authorization", authorizationScopes)); return res; } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("CloudE 接口文档") .description("物华天宝 , 龙光射牛斗之墟 \r" + "人杰地灵 , 徐孺下陈蕃之榻\r" + "------CloudE 接口文档") .termsOfServiceUrl("www.baidu.com") .contact(new Contact("【楚辞】", "http://localhost:8081/doc.html", "自己邮箱地址")) .version("1.0") .build(); } }

同时为了测试,我们新建了HelloController。java文件进行Swagger的测试
我们通过域名:端口号/doc,html来访问swagger
在这里插入图片描述
Cloud E随笔-后端_piece3--实现登录功能_第4张图片

3. 登录验证

未登录状态下去请求接口:
Cloud E随笔-后端_piece3--实现登录功能_第5张图片
提示未登录,需要登录。
Cloud E随笔-后端_piece3--实现登录功能_第6张图片

4. 添加验证码模块

4.1 添加验证码依赖

这里采用谷歌的验证码解决方案

dependency>
        
        <dependency>
            <groupId>com.github.axetgroupId>
            <artifactId>kaptchaartifactId>
            <version>0.0.9version>
        dependency>

4.2 添加验证码配置文件

在这里插入图片描述
新建CaptchaConfig配置文件,在这里进行验证码的一些设置,如边框,字体大小,字体样式等等。

package com.chuci.server.config.captcha;

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;

/**
 * 验证码配置类
 *
 * @Auther chuci
 * @Data 2022-01-15 18:04
 * @Description:
 */
@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,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 = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }

}

4.3 验证码生成

package com.chuci.server.controller;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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;

/**
 * 验证码
 *
 * @Auther chuci
 * @Data 2022-01-15 18:14
 * @Description:
 */
@RestController
public class CaptchaController {

    @Autowired
    private DefaultKaptcha defaultKaptcha;

    @ApiOperation(value = "验证码")
    @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, mustrevalidate");
        // 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();
                }
            }
        }
        //-------------------生成验证码 end --------------------------
    }
}

重启项目,我们可以看到swagger出现验证码模块,并且测试成功生成验证码

Cloud E随笔-后端_piece3--实现登录功能_第7张图片
Cloud E随笔-后端_piece3--实现登录功能_第8张图片

4.4 修改登录传递参数

  1. 修改AdminLoginParam文件,新增验证码
    在这里插入图片描述
    Cloud E随笔-后端_piece3--实现登录功能_第9张图片
    Cloud E随笔-后端_piece3--实现登录功能_第10张图片
  2. 修改登录Controller文件,传递参数新增验证码部分
    Cloud E随笔-后端_piece3--实现登录功能_第11张图片
    Cloud E随笔-后端_piece3--实现登录功能_第12张图片
    同时修改Service层文件以及实现Imp层文件,并进行验证码正确性的检测
    Cloud E随笔-后端_piece3--实现登录功能_第13张图片
    Cloud E随笔-后端_piece3--实现登录功能_第14张图片
//        验证码检测
        String captcha = request.getSession().getAttribute("captcha").toString();
        System.out.println("captcha:" + captcha + "; code:" + code);
        if(!StringUtils.hasLength(code) || !captcha.equalsIgnoreCase(code)){
            return SysResult.error("验证码错误,请重新输入!");
        }

5. 登录成功

  1. 验证码错误
    Cloud E随笔-后端_piece3--实现登录功能_第15张图片
  2. 登录成功
    Cloud E随笔-后端_piece3--实现登录功能_第16张图片
  3. 登录 成功,将返回的data信息中 tokenHead、token分别粘贴到swagger中Authorize页面参数值输入框中作为登录token令牌,tokenHead与token之间使用空格隔开
"tokenHead": "Bearer"
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE2NDI2NjUwMzc4OTYsImV4cCI6MTY0MzI2OTgzN30.iVkznvdJ3DCCVSdVbCBN34nz_BG1JGXyfolJ5GLH7_uIZvEcMHLAzc6q8Hkzqx8AX_d7VEH_wk20mrbtk3HvgA"

Cloud E随笔-后端_piece3--实现登录功能_第17张图片
再次进行接口调用,则会带如刚刚的参数值token令牌进行登录验证。
Cloud E随笔-后端_piece3--实现登录功能_第18张图片
Cloud E随笔-后端_piece3--实现登录功能_第19张图片
Cloud E随笔-后端_piece3--实现登录功能_第20张图片
致此,整个登录模块已完成,等待前端页面调用验证即可。


至此,本节完~~~

上一节: Cloud E随笔-后端_piece2–代码生成器
下一节:

此 系 列 以 完 整 记 录 自 己 项 目 经 历 此系列以完整记录自己项目经历

你可能感兴趣的:(Java项目组,后端,java,spring)