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

一、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依赖

<!--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.1</version>
</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类

在这里插入图片描述
实现UserDetails,重写其方法,将所有返回类型改为 true,但注意isEnabled()方法是否启用账号,返回值返回Admin类中enable属性值,因此isEnabled()方法返回值为enabled
在这里插入图片描述
在这里插入图片描述

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;

/**
 * <p>
 * 
 * </p>
 *
 * @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;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @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界面

<!-- swagger2 依赖 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!-- Swagger第三方ui依赖 -->
<!--        太丑了,颜色太亮-->
<!--        <dependency>-->
<!--            <groupId>com.github.xiaoymin</groupId>-->
<!--            <artifactId>swagger-bootstrap-ui</artifactId>-->
<!--            <version>1.9.6</version>-->
<!--        </dependency>-->
        <!--解决集成knife4j时冲突问题-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>31.0.1-jre</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-micro-spring-boot-starter</artifactId>
            <version>2.0.5</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>2.0.5</version>
        </dependency>

2. Swagger2配置

在config文件夹下创建新文件夹swagger存放Swagger2配置类。新建SwaggerConfig.java文件
在这里插入图片描述
在这里进行简单常用配置

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。
     * <p>
     * 再通过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
在这里插入图片描述
在这里插入图片描述

3. 登录验证

未登录状态下去请求接口:
在这里插入图片描述
提示未登录,需要登录。
在这里插入图片描述

4. 添加验证码模块

4.1 添加验证码依赖

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

</dependency>
        <!-- google kaptcha依赖 -->
        <dependency>
            <groupId>com.github.axet</groupId>
            <artifactId>kaptcha</artifactId>
            <version>0.0.9</version>
        </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出现验证码模块,并且测试成功生成验证码

在这里插入图片描述
在这里插入图片描述

4.4 修改登录传递参数

  1. 修改AdminLoginParam文件,新增验证码
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. 修改登录Controller文件,传递参数新增验证码部分
    在这里插入图片描述
    在这里插入图片描述
    同时修改Service层文件以及实现Imp层文件,并进行验证码正确性的检测
    在这里插入图片描述
    在这里插入图片描述
//        验证码检测
        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. 验证码错误
    在这里插入图片描述
  2. 登录成功
    在这里插入图片描述
  3. 登录 成功,将返回的data信息中 tokenHead、token分别粘贴到swagger中Authorize页面参数值输入框中作为登录token令牌,tokenHead与token之间使用空格隔开
"tokenHead": "Bearer"
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE2NDI2NjUwMzc4OTYsImV4cCI6MTY0MzI2OTgzN30.iVkznvdJ3DCCVSdVbCBN34nz_BG1JGXyfolJ5GLH7_uIZvEcMHLAzc6q8Hkzqx8AX_d7VEH_wk20mrbtk3HvgA"

在这里插入图片描述
再次进行接口调用,则会带如刚刚的参数值token令牌进行登录验证。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
致此,整个登录模块已完成,等待前端页面调用验证即可。


至此,本节完~~~

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值