参考了很多网上优秀的教程,结合自己的理解,实现了登录认证功能,不打算把理论搬过来,直接上代码可能入门更快,文中说明都是基于我自己的理解写的,可能存在表述或者解释不对的情况,如果需要理论支撑,可以网上在找一下相关文章学习,或者直接访问框架官网Hello Spring Security :: Spring Security
本文主要通过IDEA创建spring boot工程,并加载mybatis和spring security相关配置,完成系统登录认证功能
点击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=.jsp
server.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=root
mybatis.mapper-locations = classpath:com/sgp/ss/mapper/*.xml
完整的代码目录如下:
mybatis主要的配置涉及以下几个地方:
因为在代码目录中,*Mappee.xml文件放在了main/src/java目录下,所以需要在pom文件中配置不要过来/java目录下是xml文件,如果不添加这个配置,那么在target文件包下就没有xml文件,项目就会报Invalid bound statement (not found)的错误,而且一定要注意标签include内的内容,一定要写成**/*xml这个形式,其他形式都不要写
配置xml文件的扫描位置,这个需要在配置文件application.properties中配置,建议写完整路径,这样不容易出错,一定要注意,路径中不能出现空格等特殊符号!
最后开启项目启动后对mapper文件进行扫描,需要在启动类上添加注解
@MapperScan(basePackages = "com.sgp.ss.dao")
上述配置完成后,编写数据库表以及相应的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);
@Autowired
private IUserMapper userMapper;
@Override
public void truncate() {
}
@Override
public 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;
}
}
@Override
public 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);
}
}
@Override
public void delete(Long id) {
userMapper.delete(id);
}
@Override
public 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;
}
@Override
public 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 = id
where 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 user
where 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 user
where 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 {
@Autowired
private 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的核心就是过滤器链,框架提供了十几种过滤器,其中比较主要的是认证过滤器和JWT过滤器,另外security对用户信息和用户服务功能进行了规范封装,即接口UserDetails和UserDetailsService,所以如果要自定义过滤器,除了要复写几个过滤器外,还要编写用户信息去实现规范用户接口UserDetails和UserDetailsService
首先编写用户登录信息对象实现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;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
/**
* 账户是否过期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 账户是否被锁定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 证书是否过期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 账户是否有效
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
}
然后编写用户服务实现类,实现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 {
@Autowired
IUserMapper userMapper;
@Override
public 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;
}
}
一般登录认证的流程中都会涉及到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 {
@Autowired
private UserEntityService userEntityService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
// @Autowired
// private JwtProperties jwtProperties;
@Override
protected 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();
}
}
第二个需要复写的过滤器是认证过滤器,这个过滤器的功能就是从输入流中获取用户信息,生成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 {
// @Autowired
private 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);
}
@Override
public 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);
}
@Override
protected 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);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
response.getWriter().write("authentication failed");
}
}
最后就是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 {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
// @Bean
@Override
protected 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
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public 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 {
@Override
public 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 {
@Override
public 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();
}
}
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);
}
// 从得到的令牌里面获取用户ID
public 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进行测试
首先验证配置类WebSecurityConfig中放行的/hello地址,这个地址访问的时候不会经过框架验证,直接会得到相应
然后验证登录页面,使用用户名密码首次登录,会返回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返回给了用户
下一步我们验证数据库的查询接口,也就是index.java中的第二个接口,同时也验证我们的mybatis是否集成成功,这时候需要注意的是,我们需要携带着用户生成的token去访问,否则就会验证失败
首先验证不携带token的情况,,请求头里的token没有打勾,可以看到会直接调用我们之前复写的认证不成功的返回信息
然后我们给请求头里的token打勾,再次发送,可以看到得到了正确的响应内容
至此,所有功能测试完毕