spring boot集成mybatis和springsecurity实现登录认证功能

参考了很多网上优秀的教程,结合自己的理解,实现了登录认证功能,不打算把理论搬过来,直接上代码可能入门更快,文中说明都是基于我自己的理解写的,可能存在表述或者解释不对的情况,如果需要理论支撑,可以网上在找一下相关文章学习,或者直接访问框架官网Hello Spring Security :: Spring Security

本文主要通过IDEA创建spring boot工程,并加载mybatis和spring security相关配置,完成系统登录认证功能

创建spring boot工程

点击File->New->Project,分别按照下述步骤创建spring boot工程

依次在Developer Tools选择Spring Boot DevTools、Lombok

Security选择spring security

在SQL目录下选择MyVatis Framework 和MySQL Driver,右侧可以看到已经选择的框架,上方可以选择spring boot的版本

这几项选完以后,spring boot就会自动帮我们加载好这些框架\依赖

因为spring boot的新版本不太熟悉,可以改成比较成熟的2.4.2版本

因为后边的开发中,会继续添加几个相关的依赖,所以后边的内容均以已经完成的完整项目代码为例进行说明

完整的pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.2</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.sgp</groupId><artifactId>ss</artifactId><version>0.0.1-SNAPSHOT</version><name>ss</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version><mysql-connector-java.version>8.0.17</mysql-connector-java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.0</version></dependency><!--<dependency>--><!--<groupId>org.thymeleaf.extras</groupId>--><!--<artifactId>thymeleaf-extras-springsecurity6</artifactId>--><!--</dependency>--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql-connector-java.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--<dependency>--><!--<groupId>org.mybatis.spring.boot</groupId>--><!--<artifactId>mybatis-spring-boot-starter-test</artifactId>--><!--<version>3.0.3</version>--><!--<scope>test</scope>--><!--</dependency>--><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.19.2</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency><dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.5</version></dependency><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-jpa</artifactId><version>2.4.6</version></dependency><dependency><groupId>org.hibernate</groupId><artifactId>hibernate-entitymanager</artifactId><version>4.2.1.Final</version></dependency></dependencies><build><resources><!--<resource>--><!--<directory>src/main/java/com/sgp/ss/mapper</directory>--><!--<targetPath>mapper</targetPath>--><!--</resource>--><!--<resource>--><!--<directory>src/main/resources</directory>--><!--</resource>--><resource><directory>src/main/java</directory><includes><!--<include>**/*.properties</include>--><include>**/*.xml</include></includes><filtering>false</filtering></resource><!--<resource>--><!--<directory>src/main/resources</directory>--><!--<includes>--><!--<include>**/*.properties</include>--><!--<include>**/*.xml</include>--><!--</includes>--><!--<filtering>false</filtering>--><!--</resource>--></resources><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>

完整配置文件application.properties如下:

spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jspserver.port=8080
spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false
spring.main.allow-bean-definition-overriding=true# Show or not log for each sql query
spring.jpa.database = MYSQL
spring.jpa.show-sql = true
spring.jpa.open-in-view=false# database
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=rootmybatis.mapper-locations = classpath:com/sgp/ss/mapper/*.xml

完整的代码目录如下:

mybatis配置与代码

mybatis主要的配置涉及以下几个地方:

1,pom文件配置

因为在代码目录中,*Mappee.xml文件放在了main/src/java目录下,所以需要在pom文件中配置不要过来/java目录下是xml文件,如果不添加这个配置,那么在target文件包下就没有xml文件,项目就会报Invalid bound statement (not found)的错误,而且一定要注意标签include内的内容,一定要写成**/*xml这个形式,其他形式都不要写

这部分配置可以参考BindingException:Invalid bound statement (not found)异常_bindingexception: invalid bound statement-CSDN博客

2,application.properties文件配置

配置xml文件的扫描位置,这个需要在配置文件application.properties中配置,建议写完整路径,这样不容易出错,一定要注意,路径中不能出现空格等特殊符号!

3,启动类注解

最后开启项目启动后对mapper文件进行扫描,需要在启动类上添加注解

@MapperScan(basePackages = "com.sgp.ss.dao")

4,相关代码

上述配置完成后,编写数据库表以及相应的dao,dto,entity,service,mapper文件,然后application.properties文件中配置数据库,把所有数据库的相关代码编写完成,比如测试登录验证用的用户信息表,具体如下:

#数据库建表语句 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `permission` varchar(255) DEFAULT NULL, `role` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1

#entity对象

package com.sgp.ss.domain.entity;/*** @author shanguangpu* @date 2023/2/22 17:33*/
public class UserEntity {private Long id;private String username;private String password;private String permission;private String role;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public String getPermission() {return permission;}public void setPermission(String permission) {this.permission = permission;}public String getRole() {return role;}public void setRole(String role) {this.role = role;}
}

#dto对象

package com.sgp.ss.domain.dto.data;/*** @author shanguangpu* @date 2023/2/22 17:32*/
public class UserDto {private Long id;private String username;private String password;private String permission;private String role;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public String getPermission() {return permission;}public void setPermission(String permission) {this.permission = permission;}public String getRole() {return role;}public void setRole(String role) {this.role = role;}
}

#DAO代码

package com.sgp.ss.dao;import com.sgp.ss.domain.dto.data.UserDto;
import com.sgp.ss.domain.entity.UserEntity;
import org.springframework.stereotype.Repository;import java.util.List;/*** @author shanguangpu* @date 2023/2/22 17:34*/
@Repository
public interface IUserMapper {public void truncate();/*** 新增对象** @param* @return*/public void insert(UserEntity userEntity);/*** 更新对象** @param* @return*/public void update(UserEntity userEntity);/*** 删除记录** @param* @return*/public void delete(Long id);/*** 根据主键获取对象** @param id*            主键字段* @return*/public UserEntity getUserEntityById(Long id);public UserEntity getUserEntityByName(String username);/*** 根据查询Bean获取对象集合,不带翻页** @param queryBean* @return*/public List<UserEntity> queryUserEntityList(UserDto queryBean);}

#service接口文件

package com.sgp.ss.service;import com.sgp.ss.domain.dto.data.UserDto;
import com.sgp.ss.domain.entity.UserEntity;
import org.springframework.stereotype.Component;import java.util.List;/*** @author shanguangpu* @date 2023/2/22 17:49*/
@Component
public interface UserEntityService {public void truncate();/*** 新增对象** @param* @return*/public void insert(UserDto userDto);/*** 更新对象** @param* @return*/public void update(UserDto userDto);/*** 删除记录** @param* @return*/public void delete(Long id);/*** 根据主键获取对象** @param id*            主键字段* @return*/public UserEntity getUserEntityById(Long id);public UserEntity getUserEntityByName(String username);/*** 根据查询Bean获取对象集合,不带翻页** @param queryBean* @return*/public List<UserEntity> queryUserEntityList(UserDto queryBean);}

#service实现类

package com.sgp.ss.service.impl;import com.sgp.ss.dao.IUserMapper;
import com.sgp.ss.domain.dto.data.UserDto;
import com.sgp.ss.domain.entity.UserEntity;
import com.sgp.ss.service.UserEntityService;
//import com.sgp.ss.util.BeanUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.List;/*** @author shanguangpu* @date 2023/2/22 17:50*/
@Service
@Transactional
public class UserEntityServiceImpl implements UserEntityService {/** LOG */private static final Log LOG = LogFactory.getLog(UserEntityServiceImpl.class);@Autowiredprivate IUserMapper userMapper;@Overridepublic void truncate() {}@Overridepublic void insert(UserDto userDto) {try {if (null != userDto) {UserEntity entity = new UserEntity();
//                BeanUtils.copyProperties(userDto, entity);entity.setId(userDto.getId());entity.setUsername(userDto.getUsername());entity.setPassword(userDto.getPassword());entity.setPermission(userDto.getPermission());entity.setRole(userDto.getRole());userMapper.insert(entity);} else {LOG.warn("UserEntityServiceImpl#insert failed, param is illegal.");}} catch (Exception e) {LOG.warn("UserEntityServiceImpl#insert failed, UserEntity has existed.");throw e;}}@Overridepublic void update(UserDto userDto) {boolean resultFlag = false;try {if (null != userDto) {UserEntity entity = new UserEntity();entity.setId(userDto.getId());entity.setUsername(userDto.getUsername());entity.setPassword(userDto.getPassword());entity.setPermission(userDto.getPermission());entity.setRole(userDto.getRole());userMapper.update(entity);} else {LOG.warn("UserEntityServiceImpl#update failed, param is illegal.");}} catch (Exception e) {LOG.error("UserEntityServiceImpl#update has error.", e);}}@Overridepublic void delete(Long id) {userMapper.delete(id);}@Overridepublic UserEntity getUserEntityById(Long id) {UserEntity userEntity = null;try {if (null != id) {userEntity = userMapper.getUserEntityById(id);} else {LOG.warn("UserEntityServiceImpl#getUserEntityById failed, param is illegal.");}} catch (Exception e) {LOG.error("UserEntityServiceImpl#getUserEntityById has error.", e);}return userEntity;}public UserEntity getUserEntityByName(String username){UserEntity userEntity = null;try {if (null != username) {userEntity = userMapper.getUserEntityByName(username);} else {LOG.warn("UserEntityServiceImpl#getUserEntityByName failed, param is illegal.");}} catch (Exception e) {LOG.error("UserEntityServiceImpl#getUserEntityByName has error.", e);}return userEntity;}@Overridepublic List<UserEntity> queryUserEntityList(UserDto queryBean) {List<UserEntity> userEntities = null;try {userEntities = userMapper.queryUserEntityList(queryBean);} catch (Exception e) {LOG.error("UserEntityServiceImpl#queryUserEntityList has error.", e);}return userEntities;}
}

#mapper.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.sgp.ss.dao.IUserMapper"><resultMap id="UserEntityMap" type="com.sgp.ss.domain.entity.UserEntity"><result column="id" property="id" jdbcType="DECIMAL" /><result column="username" property="username" jdbcType="VARCHAR" /><result column="password" property="password" jdbcType="VARCHAR" /><result column="permission" property="permission" jdbcType="VARCHAR" /><result column="role" property="role" jdbcType="VARCHAR" /></resultMap><sql id="UserEntityColumns">id,username,password,permission,role</sql><sql id="UserEntityUsedColumns">username,password,permission,role</sql><!-- 清空表 --><delete id="truncate" parameterType="java.lang.Long">truncate table user</delete><!-- 创建信息 --><insert id="insert" parameterType="com.sgp.ss.domain.entity.UserEntity">INSERT INTO user(<include refid="UserEntityUsedColumns"/>)VALUES (#{username},#{password},#{permission},#{role})</insert><!-- 修改信息 --><update id="update" parameterType="com.sgp.ss.domain.entity.UserEntity"><if test="_parameter != null"><if test="id != null and id > 0">update user set<if test="id != null and id != ''">id = #{id,jdbcType=DECIMAL},</if><if test="username != null and username != ''">username = #{username,jdbcType=VARCHAR},</if><if test="password != null and password != ''">password = #{password,jdbcType=VARCHAR},</if><if test="permission != null and permission != ''">permission = #{permission,jdbcType=VARCHAR},</if><if test="role != null and role != ''">role = #{role,jdbcType=VARCHAR},</if>id = idwhere id = #{id}</if></if></update><!-- 删除信息-逻辑删除 --><delete id="delete" parameterType="java.lang.Long">delete from  user where id = #{id}</delete><!-- 根据主键获取对象信息 --><select id="getUserEntityById" resultMap="UserEntityMap" parameterType="java.lang.Long"><if test="_parameter != null">select <include refid="UserEntityColumns"/> from userwhere id = #{_parameter} and 1 = 1</if></select><!-- 根据名称获取对象信息 --><select id="getUserEntityByName" resultMap="UserEntityMap" parameterType="java.lang.Long"><if test="_parameter != null">select <include refid="UserEntityColumns"/> from userwhere username = #{username} and 1 = 1</if></select><!-- 根据查询Bean获取数据集合,不带翻页 --><select id="queryUserEntityList" resultMap="UserEntityMap"parameterType="com.sgp.ss.domain.dto.data.UserDto">select <include refid="UserEntityColumns"/> from user where <include refid="queryUserEntityListWhere"/></select><!-- 常用的查询Where条件 --><sql id="queryUserEntityListWhere">1 = 1<if test="id != null and id != ''">and id = #{id,jdbcType=DECIMAL}</if><if test="username != null and username != ''">and username = #{username,jdbcType=VARCHAR}</if><if test="password != null and password != ''">and password = #{password,jdbcType=VARCHAR}</if><if test="permission != null and permission != ''">and permission = #{permission,jdbcType=VARCHAR}</if><if test="role != null and role != ''">and role = #{role,jdbcType=VARCHAR}</if></sql>
</mapper>

mybatis配置完成后,可以编写controller文件进行数据库测试

package com.sgp.ss.controller;import com.sgp.ss.dao.IUserMapper;
import com.sgp.ss.domain.dto.data.UserDto;
import com.sgp.ss.domain.entity.UserEntity;
import com.sgp.ss.security.LoginUser;
import com.sgp.ss.util.JwtTokenUtil;
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.RestController;import java.util.List;/*** @author shanguangpu* @date 2023/12/12 14:59*/
@RestController
public class index {@Autowiredprivate IUserMapper userMapper;@GetMapping(value = "hello")public String request(){System.out.println("hello word");return "hello success";}@PostMapping(value = "query")public String selectInfo(){List<UserEntity> userEntities = userMapper.queryUserEntityList(new UserDto());return userEntities.toString();}}

由于已经添加了spring security登录验证功能,所以代码中在没有配置spring security的前提下,数据库的测试会被登录验证拦截,所以可以在配置完成security后一并进行测试

spring security配置与代码

spring security的核心就是过滤器链,框架提供了十几种过滤器,其中比较主要的是认证过滤器和JWT过滤器,另外security对用户信息和用户服务功能进行了规范封装,即接口UserDetails和UserDetailsService,所以如果要自定义过滤器,除了要复写几个过滤器外,还要编写用户信息去实现规范用户接口UserDetails和UserDetailsService

1,UserDetails接口实现

首先编写用户登录信息对象实现UserDetails接口,这里为了区别用于数据库查询的用户类,新建Java类继承已有用户类,并实现UserDetails接口。代码如下

package com.sgp.ss.security;import com.sgp.ss.domain.entity.UserEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;
import java.util.HashSet;
import java.util.Set;/*** @author shanguangpu* @date 2023/11/30 15:58*/
public class LoginUser extends UserEntity implements UserDetails {Collection<? extends GrantedAuthority> authorities;private Set<String> permissions = new HashSet<>();public LoginUser(UserEntity userEntity){if (null != userEntity){this.setUsername(userEntity.getUsername());this.setPassword(userEntity.getPassword());}}public LoginUser(UserEntity userEntity, Collection<? extends GrantedAuthority> authorities){this.setId(userEntity.getId());this.setUsername(userEntity.getUsername());this.setPassword(userEntity.getPassword());permissions.add(userEntity.getPermission());this.authorities = authorities;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}/*** 账户是否过期* @return*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** 账户是否被锁定* @return*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** 证书是否过期* @return*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 账户是否有效* @return*/@Overridepublic boolean isEnabled() {return true;}
}
2,UserDetailsService接口实现

然后编写用户服务实现类,实现UserDetailsService接口,这里主要复写loadUserByUsername(String username)这个方法,这个方法就是去数据库查询用户信息,并返回UserDetails对象,代码如下:

package com.sgp.ss.security;import com.sgp.ss.dao.IUserMapper;
import com.sgp.ss.domain.entity.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;/*** @author shanguangpu* @date 2023/11/30 15:50*/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {@AutowiredIUserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {try {UserEntity userEntityByName = userMapper.getUserEntityByName(username);if (userEntityByName == null){throw new UsernameNotFoundException("用户"+username+"不存在");}return new LoginUser(userEntityByName);} catch (Exception e){e.printStackTrace();}return null;}
}
3,过滤器自定义-JWT过滤器

一般登录认证的流程中都会涉及到token,它可以理解为一个标识令牌,用户在第一次登录的时候系统会返回一个令牌token给用户,该用户在下次登录的时候,就不需要携带用户名密码进行请求了,只需要把token带上,系统可以从token中解析出用户信息,对用户进行验证;如果token在有效期内,则系统就会对请求放行,如果过期,则需要重新携带用户名密码进行登录然后再发送请求,并生成新的token返回给用户,所以我们的JWT过滤器主要就是对token进行验证,如果token有效且不为空,则通过token可以获取到用户信息,如果用户信息验证正确,就调用security框架的UsernamePasswordAuthenticationToken方法,生成security框架用户认证信息token,并存放在框架上下文SecurityContextHolder中,以便在下次登录中进行校验,最后过滤器放行,进行下一个过滤器,如果token为空,则直接放行过滤器,进入下一个过滤器生成token返回给用户,代码如下:

package com.sgp.ss.security;import com.sgp.ss.domain.entity.UserEntity;
import com.sgp.ss.service.UserEntityService;
import com.sgp.ss.util.JwtTokenUtil;
import com.sgp.ss.vo.JwtProperties;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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;/*** @author shanguangpu* @date 2023/12/8 15:29*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate UserEntityService userEntityService;@Autowiredprivate JwtTokenUtil jwtTokenUtil;//    @Autowired
//    private JwtProperties jwtProperties;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取token信息final String authorization = request.getHeader("token");String name = null;String authToken = null;if (!StringUtils.isEmpty(authorization)) {authToken = authorization.replace("Authorization", "");try {name = jwtTokenUtil.getUsernameFromToken(authToken);} catch (ExpiredJwtException e){e.printStackTrace();}}if (name != null && SecurityContextHolder.getContext().getAuthentication() == null){
//            if (jwtTokenUtil.isTokenValid(name, authToken)){UserEntity userEntityByName = userEntityService.getUserEntityByName(name);UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userEntityByName, null, jwtTokenUtil.getAuthorityFromToken(authToken));SecurityContextHolder.getContext().setAuthentication(authentication);
//            }}filterChain.doFilter(request, response);}public JwtAuthenticationTokenFilter(){super();}
}
4,过滤器自定义-Authentication认证过滤器

第二个需要复写的过滤器是认证过滤器,这个过滤器的功能就是从输入流中获取用户信息,生成security的token令牌,交给认证管理器AuthenticationManager,

对于登录成功的用户,将生成框架定义的UsernamePasswordAuthenticationToken保存到上下文中,然后调用复写的成功验证方法successfulAuthentication生成token返回给用户,同时也可以复写验证失败的方法,自定义验证失败后的返回信息,代码如下

package com.sgp.ss.security;import com.fasterxml.jackson.databind.ObjectMapper;
import com.sgp.ss.util.JwtTokenUtil;
import com.sgp.ss.vo.AuthRequestVO;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** @author shanguangpu* @date 2023/12/11 14:57*/
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {//    @Autowiredprivate AuthenticationManager authenticationManager;private JwtTokenUtil jwtTokenUtil;/*** 这个方法的目的仅仅是为了修改一下默认的登录接口,其他参数均是为了通过WebSecurityConfig配置类实现在本方法中自动注入前边private的类* @param tokenUtil* @param authenticationManager* @param url*/public CustomAuthenticationFilter(JwtTokenUtil tokenUtil, AuthenticationManager authenticationManager, String url) {this.jwtTokenUtil = tokenUtil;//传入这个参数的目的,是为了在WebSecurityConfig配置类中将该类注入this.authenticationManager = authenticationManager;//传入这个参数的目的,是为了在WebSecurityConfig配置类中将该类注入super.setFilterProcessesUrl(url);}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {String username = "";String password = "";//从输入流中获取登录信息try {AuthRequestVO requestVO = new ObjectMapper().readValue(request.getInputStream(), AuthRequestVO.class);username = requestVO.getUsername();password = requestVO.getPassword();if (StringUtils.isBlank(password)){throw new BadCredentialsException("Password cannot be null");}} catch (IOException e){e.printStackTrace();}UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);this.setDetails(request, authRequest);return authenticationManager.authenticate(authRequest);}@Overrideprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, Authentication authResult) throws IOException, ServletException {LoginUser userDetails = (LoginUser) authResult.getPrincipal();String token = jwtTokenUtil.generateToken(userDetails);response.setHeader("token", token);}@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {response.getWriter().write("authentication failed");}}
5,WebSecurityConfig配置类

最后就是spring security的配置类,主要配置用户的密码存储编码,http请求的过滤条件,过滤器的顺序等,代码如下:

package com.sgp.ss.config;import com.sgp.ss.security.*;
import com.sgp.ss.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;/*** @author shanguangpu* @date 2023/11/30 15:25*/
@Configuration
@EnableWebSecurity
//@EnableGlobalMethodSecurity(prePostEnabled = true)
//@EnableConfigurationProperties(JwtProperties.class)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsServiceImpl userDetailsService;@AutowiredJwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception{auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}//    @Bean@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable()//关闭csrf.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//不通过session获取securityContext.and().authorizeRequests()
//                .antMatchers("/").permitAll().antMatchers("/hello").permitAll()
//                .antMatchers(HttpMethod.POST).permitAll()
//                .antMatchers("/user/login").anonymous()//对于登录接口,允许匿名访问.anyRequest().authenticated();//所有请求都要经过鉴权认证http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint()).accessDeniedHandler(new CustomAccessDeniedHandler());//把token校验过滤器添加到过滤器链中http.addFilter(new CustomAuthenticationFilter(jwtTokenUtil, authenticationManager(), "/user/login"));http.addFilterBefore(jwtAuthenticationTokenFilter, CustomAuthenticationFilter.class);http.cors();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Beanpublic static PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}public static void main(String[] args) {String encode = passwordEncoder().encode("4321");System.out.println(encode);}}

上述配置中,还有两个认证失败和权限失败两个处理异常类,代码如下:

package com.sgp.ss.security;import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** @author shanguangpu* @date 2023/12/8 16:00*/
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {Throwable t = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);response.setContentType("application/json;charset=utf-8");response.getWriter().write("{\"code\":\"403\",\"message\":\"请重新登录\"}");}
}
package com.sgp.ss.security;import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;/*** @author shanguangpu* @date 2023/12/8 15:59*/
public class CustomAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.setContentType("application/json;charset=utf-8");PrintWriter out = response.getWriter();out.write("{\"code\":\"403\",\"message\":\"权限不足\"}");out.flush();out.close();}
}
6,JWT工具类
package com.sgp.ss.util;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.sgp.ss.security.LoginUser;
import com.sgp.ss.vo.JwtProperties;
import io.jsonwebtoken.*;
import io.jsonwebtoken.impl.DefaultClock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import java.io.Serializable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;/*** @author shanguangpu* @date 2023/2/22 10:42*/
@Component
public class JwtTokenUtil implements Serializable {private static final long serialVersionUID = -1264536648286114018L;private Clock clock = DefaultClock.INSTANCE;private Map<String, String> tokenMap = new ConcurrentHashMap<>(32);public String generateToken(LoginUser loginUser){Map<String, Object> claims = new HashMap<>();claims.put("username", loginUser.getUsername());claims.put("authority", loginUser.getAuthorities());String token = generateToken(claims, loginUser.getId() + "");if (! StringUtils.isEmpty(token)){}return token;}/*** 生成token* @param claims* @param subject* @return*/private String generateToken(Map<String, Object> claims, String subject) {final Date createdDate = clock.now();final Date expirationDate = generateExpirationDate(createdDate);return Jwts.builder()// 自定义属性.setClaims(claims).setSubject(subject)// 创建时间.setIssuedAt(createdDate)// 过期时间.setExpiration(expirationDate)// 签名算法及秘钥.signWith(SignatureAlgorithm.HS512, "sgpsgp").compact();}private Date generateExpirationDate(Date createdDate) {return new Date(createdDate.getTime() + 21600 * 1000);}// 从得到的令牌里面获取用户IDpublic Integer getUserIdFromToken(String token) {Integer id = null;try {final Claims claims = getClaimsFromToken(token);id = Integer.parseInt(claims.getSubject());return id;} catch (Exception e) {}return id;}// 从得到的令牌里面获取用户名public String getUsernameFromToken(String token) {String username;try {final Claims claims = getClaimsFromToken(token);username = (String) claims.get("username");} catch (Exception e) {username = null;}return username;}// 从得到的令牌里面获取权限public List<GrantedAuthority> getAuthorityFromToken(String token) {List<GrantedAuthority> list = null;try {final Claims claims = getClaimsFromToken(token);list = (List<GrantedAuthority>)claims.get("authority");return list;} catch (Exception e) {}return list;}// 从得到的令牌里面获取创建时间public Date getCreatedDateFromToken(String token) {Date created;try {final Claims claims = getClaimsFromToken(token);created = claims.getIssuedAt();} catch (Exception e) {created = null;}return created;}// 从得到的令牌里面获取过期时间public Date getExpirationDateFromToken(String token) {Date expiration;try {final Claims claims = getClaimsFromToken(token);expiration = claims.getExpiration();} catch (Exception e) {expiration = null;}return expiration;}private Claims getClaimsFromToken(String token) {Claims claims;try {claims = Jwts.parser().setSigningKey("sgpsgp").parseClaimsJws(token).getBody();} catch (Exception e) {claims = null;}return claims;}/*** 检查token是否过期* @param token* @return*/public Boolean isTokenExpired(String token) {final Date expiration = getExpirationDateFromToken(token);boolean flag = expiration.before(new Date());if(flag) {String username = getUsernameFromToken(token);if(username != null) {tokenMap.remove(username);}}return flag;}public Boolean isTokenValid(String username, String token) {if(tokenMap.get(username) != null && tokenMap.get(username).equals(token.trim())) {return !isTokenExpired(token);}return false;}public Boolean isTokenValid(int id, String token) {if (tokenMap.get(id) != null && tokenMap.get(id).equals(token.trim())) {return !isTokenExpired(token);}return false;}public void deleteToken(String token) {String username = getUsernameFromToken(token);if(username != null) {tokenMap.remove(username);}}public static void main(String[] args) {HashMap<String, Object> map = new HashMap<>();Calendar calendar = Calendar.getInstance();calendar.add(Calendar.SECOND, 60);String token = JWT.create().withHeader(map).withClaim("userName", "aaa").withClaim("password", "1234").withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256("abcd"));System.out.println(token);JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("abcd")).build();DecodedJWT verify = jwtVerifier.verify(token);System.out.println(verify.getClaim("userName").asString());System.out.println(verify.getClaim("password").asString());}}

postman测试

所有代码编写完成后,可以通过postman进行测试

1,不走登录验证的接口测试

首先验证配置类WebSecurityConfig中放行的/hello地址,这个地址访问的时候不会经过框架验证,直接会得到相应

2,登陆页面返回token测试

然后验证登录页面,使用用户名密码首次登录,会返回token,这个用户名密码需要提前在数据库里创建,为了安全起见,用户密码采用加密的密文存储,加密算法就是代码中配置的BCryptPasswordEncoder()方法,这个可以通过一个简单的Java函数得到,具体如下:

public static PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();
}public static void main(String[] args) {String encode = passwordEncoder().encode("4321");System.out.println(encode);
}

在数据库里存的样式就是下图的样子

在使用postman进行测似的时候,请求体里的用户密码使用明文即可,因为我们在配置类WebSecurityConfig中配置了默认登录地址为/user/login,所以访问这个地址就会返回token,这里注意,登录需要使用POST请求

因为我们没有编写登录成功后的页面,所以响应的body是空的,但是响应头里会看到系统生成的token返回给了用户

3,携带token请求测试

下一步我们验证数据库的查询接口,也就是index.java中的第二个接口,同时也验证我们的mybatis是否集成成功,这时候需要注意的是,我们需要携带着用户生成的token去访问,否则就会验证失败

首先验证不携带token的情况,,请求头里的token没有打勾,可以看到会直接调用我们之前复写的认证不成功的返回信息

然后我们给请求头里的token打勾,再次发送,可以看到得到了正确的响应内容

至此,所有功能测试完毕

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/223681.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

camunda流程引擎——Java集成Camunda(上)(笔记)

目录 一、以一个处理流程开始1.1 后端1.2 前端1.3 执行 二、Camunda的补充2.1 使用方式2.2 可视化平台的Cockpit2.3 流程相关数据2.4 表介绍2.5 前端集成Modeler 三、用Java集成Camunda3.1 集成配置3.2 自动部署3.2.1 修改process.xml位置3.2.2 多进程引擎配置与多租户 3.3 历史…

typedef的使用

在C语言中&#xff0c;有一个关键字叫做typedef&#xff0c;有些人对此感到很疑惑。不熟悉此知识的同学都会对编程失去细心&#xff0c;直接劝退&#xff08;因为之前我就是这样&#xff09;。、 因为好不容易认识了C语言中所有的关键字&#xff08;就是类型吧&#xff0c;像啥…

详细教程 - 从零开发 Vue 鸿蒙harmonyOS应用 第一节

关于使用Vue开发鸿蒙应用的教程,我这篇之前的博客还不够完整和详细。那么这次我会尝试写一个更加完整和逐步的指南,从环境准备,到目录结构,再到关键代码讲解,以及调试和发布等,希望可以让大家详实地掌握这个过程。 一、准备工作 下载安装 DevEco Studio 下载地址&#xff1a;…

逻辑回归的介绍和应用

逻辑回归的介绍 逻辑回归&#xff08;Logistic regression&#xff0c;简称LR&#xff09;虽然其中带有"回归"两个字&#xff0c;但逻辑回归其实是一个分类模型&#xff0c;并且广泛应用于各个领域之中。虽然现在深度学习相对于这些传统方法更为火热&#xff0c;但实…

基础算法(3):排序(3)插入排序

1.插入排序实现 插入排序的工作原理是&#xff1a;通过构建有序序列&#xff0c;对于未排序数据&#xff0c;在已经排序的序列从后向前扫描&#xff0c;找到位置并插入&#xff0c;类似于平时打扑克牌时&#xff0c;将牌从大到小排列&#xff0c;每次摸到一张牌就插入到正确的位…

12.4~12.14概率论复习与相应理解(学习、复习、备考概率论,这一篇就够了)

未分配的题目 概率计算&#xff08;一些转换公式与全概率公式&#xff09;与实际概率 &#xff0c;贝叶斯 一些转换公式 相关性质计算 常规&#xff0c;公式的COV与P 复习相关公式 计算出新表达式的均值&#xff0c;方差&#xff0c;再套正态分布的公式 COV的运算性质 如…

前后端项目,nginx部署前端项目后刷新浏览器报错404的问题

问题&#xff1a; Vue单页应用项目打包部署Nginx服务器后&#xff0c;刷新页面后&#xff0c;出现404。 原因&#xff1a; 加载单页应用后路由改变均由浏览器处理&#xff0c;而刷新时将会请求当前的链接&#xff0c;而Nginx无法找到对应的页面。 解决&#xff1a; 在Nginx配…

python爬虫学习-批量爬取图片

python爬虫学习-批量爬取图片 爬虫步骤爬取前十页图片到本地根据页码获取网络源码使用xpath解析网页解析网页并下载图片主函数如下 爬取的网站为站长素材&#xff08;仅做学习使用&#xff09; 爬取的目标网站为 https://sc.chinaz.com/tupian/qinglvtupian.html如果爬取多页&…

大数据讲课笔记1.2 Linux用户操作

文章目录 零、学习目标一、导入新课二、新课讲解&#xff08;一&#xff09;用户账号管理1、用户与用户组文件2、用户账号管理工作 &#xff08;二&#xff09;用户操作1、切换用户&#xff08;1&#xff09;语法格式&#xff08;2&#xff09;切换到普通用户&#xff08;3&…

NVH软件导入音频文件

我们经常会遇到一种情况是&#xff1a;车主上下班路上经常会听到一个异响&#xff0c;但车交到我们手上&#xff0c;我们怎么在外面去试车&#xff0c;都听不到这个异响&#xff0c;或者条件达不到重现不了这个异响。如果是这样&#xff0c;我们是不是有点崩溃&#xff1f;但&a…

jstree组件的使用详细教程,部分案例( PHP / fastAdmin )

jstree 组件的使用。 简介&#xff1a;JsTree是一个jquery的插件&#xff0c;它提交一个非常友好并且强大的交互性的树&#xff0c;并且是完全免费或开源的&#xff08;MIT 许可&#xff09;。Jstree技持Html 或 json格式的的数据&#xff0c; 或者是ajax方式的动态请求加载数…

公司团队规范研发流程概要

一、背景 ● 背景&#xff1a;XXX研发部门开发流程步骤以及开发工具&#xff0c;依赖版本&#xff0c;开发规范等相关信息。 ● 技术定位&#xff1a;All。 ● 目标群体&#xff1a;所有相关研发部门技术人员。 二、操作步骤 2.1 开发前的准备 准备工作一 开发相关账号开通…

中职网络安全应急响应—Server2228

应急响应 任务环境说明: 服务器场景:Server2228(开放链接) 用户名:root,密码:p@ssw0rd123 1. 找出被黑客修改的系统别名,并将倒数第二个别名作为Flag值提交; 通过用户名和密码登录系统 在 Linux 中,利用 “alias” 命令去查看当前系统中定义的所有别名 flag:ss …

软实力篇---第二篇

系列文章目录 文章目录 系列文章目录前言一、必知必会的几点二、必须了解的两大法则三、项目经历怎么写前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。 一、必知必…

Echarts饼图中显示百分比

开发中遇到一个需求&#xff0c;要在饼图上显示数据百分比&#xff0c;下图&#xff1a; 查了echarts 文档&#xff0c;并不能通过简单的配置来实现&#xff0c;原因如下&#xff1a;在单个serie的label中&#xff0c;只能设置一个label&#xff0c;位置可以选择在饼图内部inne…

在线监控网址源码/ 网站监控工具源码/ 网站监控系统源码/定时任务/网站网址URL状态监控神器

源码介绍&#xff1a; 在线监控网址源码、 网站监控工具源码&#xff0c;它作为网站监控系统源码&#xff0c;有定时任务&#xff0c;支持卡密充值&#xff0c;是网站网址URL状态监控神器。让数据库监控更加简单和专业。远程云中监控、实时邮件告警、丰富的指标和图表、分析和…

【教学类-06-17】20231215 (题目填满55格)X-Y之间“加法题+题”

背景需求&#xff1a; 0-5加法、减法是大班孩子选择较多的题型&#xff0c;因为只有21题&#xff0c;做题速度快&#xff0c;完成后&#xff0c;&#xff0c;他们会问&#xff1a;“后面的空白格子做什么” “可以画画&#xff0c;自己出题目” 但是大部分孩子都不会自己出题目…

【导航栏内容的设置 Objective-C语言】

一、那接下来呢,我们就来做一做,关于导航控制器, 1.设置它顶部的导航栏儿内容的东西, 1)我们刚刚讲过的这个,通过代码去跳转、返回、 2)通过storyboard去跳转、返回、 但是,这两种情况,大家是不是已经注意到,导航栏里面,没有任何内容, 然后呢,返回,这是红色,…

C++ Qt开发:ProgressBar进度条组件

Qt 是一个跨平台C图形界面开发库&#xff0c;利用Qt可以快速开发跨平台窗体应用程序&#xff0c;在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置&#xff0c;实现图形化开发极大的方便了开发效率&#xff0c;本章将重点介绍ProgressBar进度条组件的常用方法及灵活运用。…