Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。提供了完善的认证机制和方法级的授权功能。是一款非常优秀的权限管理框架。它的核心是一组过滤器链,不同的功能经由不同的过滤器。这篇文章就是想通过一个小案例将Spring Security整合到SpringBoot中去。要实现的功能就是在认证服务器上登录,然后获取Token,再访问资源服务器中的资源。 ?
对比Shiro框架来说,配置会更复杂一些,但功能更强大,Shrio安全框架上手快,配置简单。
本次项目包含了:redis,mybatis-plus,jwt..?
核心过滤器:
/**
* Security 核心三种过滤器:
* UsernamePasswordAuthenticationFilter:验证账号密码的过滤器
* ExceptionTranslationFilter :异常过滤器
* FilterSecurityInterceptor :权限校验过滤器
*
*/
SpringSecurity项目依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>SpringSecurity</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.14</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.9.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!--mybatis依赖包-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok 依赖,子工程中假如需要lombok,不需要再引入-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope><!--provided 表示此依赖仅在编译阶段有效-->
</dependency>
<!--单元测试依赖,子工程中需要单元测试时,不需要再次引入此依赖了-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<!-- <scope>test</scope><!–test表示只能在test目录下使用此依赖–>-->
<exclusions>
<exclusion><!--排除一些不需要的依赖-->
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!--其它依赖...-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.9</version>
</dependency>
<!--redis应用依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.7</version>
</dependency>
</dependencies>
</project>
application.yml配置
server:
port: 8080
#管理数据源
spring:
redis:
host: 127.0.0.1
port: 6379
datasource:
#高版本驱动使用
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
#设定用户名和密码
username: root
password: root
#SpringBoot整合Mybatis
mybatis-plus:
#指定别名包
type-aliases-package: com.jt.pojo
#扫描指定路径下的映射文件
mapper-locations: classpath:/mapper/*.xml
#开启驼峰映射
configuration:
map-underscore-to-camel-case: true
# 一二级缓存默认开始 所以可以简化
#打印mysql日志
logging:
level:
com.jt.mapper: debug
?用户表及pojo类
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
`email` varchar(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` varchar(128) DEFAULT NULL COMMENT '头像',
`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* 用户表(User)实体类
*
* @author
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class User implements Serializable {
private static final long serialVersionUID = -40356785423868312L;
/**
* 主键
*/
@TableId
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
/**
* 密码
*/
private String password;
/**
* 账号状态(0正常 1停用)
*/
private String status;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phonenumber;
/**
* 用户性别(0男,1女,2未知)
*/
private String sex;
/**
* 头像
*/
private String avatar;
/**
* 用户类型(0管理员,1普通用户)
*/
private String userType;
/**
* 创建人的用户id
*/
private Long createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 删除标志(0代表未删除,1代表已删除)
*/
private Integer delFlag;
}
?权限表及pojo类
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
`path` varchar(200) DEFAULT NULL COMMENT '路由地址',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
`create_by` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* 菜单表(Menu)实体类
*
* @author makejava
* @since 2021-11-24 15:30:08
*/
@TableName(value="sys_menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {
private static final long serialVersionUID = -54979041104113736L;
@TableId
private Long id;
/**
* 菜单名
*/
private String menuName;
/**
* 路由地址
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 菜单状态(0显示 1隐藏)
*/
private String visible;
/**
* 菜单状态(0正常 1停用)
*/
private String status;
/**
* 权限标识
*/
private String perms;
/**
* 菜单图标
*/
private String icon;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
/**
* 是否删除(0未删除 1已删除)
*/
private Integer delFlag;
/**
* 备注
*/
private String remark;
}
?角色表及其他关联表
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL,
`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
`del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
`create_by` bigint(200) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(200) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
CREATE TABLE `sys_role_menu` (
`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',
PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `sys_user_role` (
`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
?redis工具类
package com.jt.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
@SuppressWarnings(value = { "unchecked", "rawtypes" })
/**
* redis 工具类
*/
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 删除Hash中的数据
*
* @param key
* @param hkey
*/
public void delCacheMapValue(final String key, final String hkey)
{
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(key, hkey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}
返回前端数据result类?
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class SysResult implements Serializable {
private Integer status; //200业务执行成功 201业务执行失败
private String msg; //服务器的提示信息
private Object data; //封装后台返回值
public static SysResult fail(){
return new SysResult(201,"业务执行失败",null);
}
public static SysResult success(){
return new SysResult(200,"业务执行成功",null);
}
//服务器返回业务数据
public static SysResult success(Object data){
return new SysResult(200,"业务执行成功",data);
}
public static SysResult success(String msg,Object data){
return new SysResult(200,msg,data);
}
}
一:引入SpringSecurity? 的 pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
当你引入security的依赖后,再访问接口 就会自动出现一个认证窗口,并且项目控制台会出现一个随机的密码
?这就是最初的security的认证。
二:设定成自己数据库账号密码,获取用户信息和权限
实际项目中认证的账号密码都必须是从我们自己的数据库中进行查询校验,所有我们要更改
验证的内容?
实现UserDetails类
这个类是用来封装用户的信息,并在loadUserByUsername方法中进行返回用户信息。
import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 此方法实现的是UserServiceImpl 中的对象 封装了用户的信息
*/
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
private List<String> permissions;
@JSONField(serialize = false) //因为下面的泛型序列化可能会出错,所有用此注解让他不进行序列化
private List<GrantedAuthority> authorities;//方便下面调用进行创建静态的
@Override //此方法是获取权限信息的 所以要进行重写
public Collection<? extends GrantedAuthority> getAuthorities() {
//把permission 中 string 类型的权限封封装成 SimpleGrantedAuthority对象
if (authorities!=null){ //此处进行判断一下,因为每次调用如果不为空则权限已经有
return authorities;
}
authorities = new ArrayList<>();
for (String permission : permissions) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true; //更改成了true
}
@Override
public boolean isAccountNonLocked() {
return true; //更改成了true
}
@Override
public boolean isCredentialsNonExpired() {
return true; //更改成了true
}
@Override
public boolean isEnabled() {
return true; //更改成了true
}
}
实现UserDetailsService类
重写loadUserByUsername方法。这个方法的重写将验证的账号密码都换成了自己数据库的
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jt.mapper.MenuMapper;
import com.jt.mapper.UserMapper;
import com.jt.pojo.LoginUser;
import com.jt.pojo.User;
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.Service;
import java.util.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
@Override //设定成自己数据库账号密码 获取用户的信息和权限
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
//如果没有查询到用户就抛出异常
if(Objects.isNull(user)){
throw new RuntimeException("用户名或者密码错误");
}
//TODO 获取权限
String s = user.getId().toString();
List<String> list = menuMapper.selectPermsByUserId(s);
//把数据封装成UserDetails返回
return new LoginUser(user,list);
}
}
创建Mapper获取用户信息和权限
创建UsweMapper并继承BaseMapper<User>进行查询用户信息。上面的代码中有引用
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.pojo.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
创建MenuMapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.pojo.Menu;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
List<String> selectPermsByUserId(String userId);
}
获取权限sql
<?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.jt.mapper.MenuMapper">
<select id="selectPermsByUserId" resultType="java.lang.String">
SELECT
DISTINCT m.`perms`
FROM
sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE
user_id = #{userId}
AND r.`status` = 0
AND m.`status` = 0
</select>
</mapper>
三:SecurityConfig配置
·注入BC加密对象
//创建BCryptPasswordEncoder 注入容器 加密方式
@Bean
public PasswordEncoder passwordEncoder(){
//当你将此对象注入容器时,就会自动将密码进行bc的比对校验。
//如果输入的明文密码与数据库中的加密密码不匹配则报错。
//切数据库中必须存储为bc加密的密码
return new BCryptPasswordEncoder();
}
测试BC加密,BC校验
bc是一个强大的APi对象,主要核心包含俩个功能。1.进行加密? 2.校验用户输入的密码与数据库中存入的密文是否一致
存入数据库中的密码应该是加密的密码。
@Test
public void test(){
//bc 加密 校验功能
//Security底层加密方式 使用BC加密
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//1.加密
String encode = bCryptPasswordEncoder.encode("1234");
System.out.println("encode = " + encode); //加密不一样的原因:密码每次加密bc会自动生成一个不同的盐
String encode2 = bCryptPasswordEncoder.encode("1234");
System.out.println("encode2 = " + encode2);
//2.校验用户输入的密码与数据库中存入的密文是否一致
boolean s = bCryptPasswordEncoder.matches("1234", "$2a$10$qvahLUr7Ngf1yNAJmvOUuOhZAFykG4BwiTVkEm2pdXsMgJgW9PVkO");
//如果密码与输入的相同则返回 true
System.out.println("s = " + s);
}
·创建JWT工具类
使用了此工具类进行生成以用户id为主的token。工具类包含了生成JWT密文,及解析JWT密文
多使用与获得token进行解析校验?
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "sangeng";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
public static void main(String[] args) throws Exception {
String jwt = createJWT("1234");
System.out.println("jwt加密 = " + jwt);
Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyZTAyOWI3Nzg0Y2I0ZjM5YmYxZDIzNDZhNDRlZTFlNCIsInN1YiI6IjEyMzQiLCJpc3MiOiJzZyIsImlhdCI6MTY1NTY4ODQzMiwiZXhwIjoxNjU1NjkyMDMyfQ.w4gyXVsHhASpWtuUrDLrSAf9trGy3OvLJHuC1-KI8Lo");
System.out.println("解密claims = " + claims.getSubject());
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
·创建 JWT认证过滤器
进行校验token
package com.jt.filter;
import com.jt.pojo.LoginUser;
import com.jt.util.JwtUtil;
import com.jt.util.RedisCache;
import io.jsonwebtoken.Claims;
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;
import java.util.Objects;
/**
* jwt认证过滤器 定义好后需要进行配置指定的位置 在SecurityConfig中进行配置
* 当调用其他接口时需要携带token,此处进行token验证,验证通过则放行
*/
@Component //token校验过滤器
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)){//如果为空
//放行 因为后面还有其他的过滤器会进行判断并提示
filterChain.doFilter(request,response);
return;
}
//解析token
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = "login:"+userId;
LoginUser loginUser = redisCache.getCacheObject(redisKey);//会根据类型自动转换
if (Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
//todo 获取权限信息封装到Authentication中 第三个为获取权限loginUser.getAuthorities()
UsernamePasswordAuthenticationToken userToken =
new UsernamePasswordAuthenticationToken(loginUser,null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(userToken);
//放行
filterChain.doFilter(request,response);
}
}
创建WebUtils 响应工具类
?是为了后面异常处理器进行响应自定义的信息数据使用。
package com.jt.util;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 往响应当中写入数据的工具栏
*/
public class WebUtils
{
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}
·创建异常处理器
当出现下面的情况时就会触发,并返回我们自己自定义的处理内容。
自定义认证失败异常处理器
import com.alibaba.fastjson.JSON;
import com.jt.util.WebUtils;
import com.jt.vo.SysResult;
import org.springframework.http.HttpStatus;
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;
@Component //自定义认证失败异常处理器
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
SysResult result = new SysResult().setStatus(401).setMsg("用户认证失败请查询登录");
String json = JSON.toJSONString(result);
//处理异常
WebUtils.renderString(response,json);
}
}
自定义授权失败处理器
import com.alibaba.fastjson.JSON;
import com.jt.util.WebUtils;
import com.jt.vo.SysResult;
import org.springframework.http.HttpStatus;
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;
@Component//授权失败处理器
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
SysResult result = new SysResult().setStatus(403).setMsg("您的权限不足");
String json = JSON.toJSONString(result);
//处理异常
WebUtils.renderString(response,json);
}
}
·创建解决跨域配置
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 解决跨越的配置
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
SecurityConfig配置
当上面的内容都配置好后,我们就可以配置完整的Security配置。
0.实现WebSecurityConfigurerAdapter类,重写其中方法进行配置
1.创建BC 注入容器,是为了使用BC的方式进行加密校验
2.注入AuthenticationManager 登录认证中心是用此对象进行封装认证
3.重写configure方法,配置登录路径,认证路径,添加过滤器,配置异常处理器,设置跨域。都是在此方法中进行设定
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启权限注解功能
import com.jt.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启权限注解功能
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPoint;
//创建BCryptPasswordEncoder 注入容器 加密方式
@Bean
public PasswordEncoder passwordEncoder(){
//当你将此对象注入容器时,就会自动将密码进行bc的比对校验。
//如果输入的明文密码与数据库中的加密密码不匹配则报错。
//切数据库中必须存储为bc加密的密码
return new BCryptPasswordEncoder();
}
@Override //配置登录的路径 及需要认证的路径
protected void configure(HttpSecurity http)throws Exception{
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
// .antMatchers("/hello").permitAll() //允许登录或者未登录都可访问
// .antMatchers("/testCors").hasAuthority("system:dept:list222")
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
//添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//配置异常处理器
http.exceptionHandling()
//配置认证失败及授权失败处理器
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
//设置跨越
http.cors();
}
@Bean
@Override //需要通过AuthenticationManager的authenticate方法进行用户认证,所有需要在此将其注入容器
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
四:登陆认证中心
登录接口
1.使用了AuthenticationManager的authenticate方法进行用户认证
2.如果认证通过则生成一个JWT的token并存入redis中(为了进行校验)。且最后返回token
3.?authenticate.getPrincipal()中存入了用户的信息,通过这个方法进行获取
4.将完整的用户信息存入redis中,userid作为key,在JWT的token校验过滤器中进行获得校验
import com.jt.pojo.LoginUser;
import com.jt.pojo.User;
import com.jt.util.JwtUtil;
import com.jt.util.RedisCache;
import com.jt.vo.SysResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Objects;
@RestController
public class LoginController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
/**
* 登录接口
* @param user
* @return
*/
@PostMapping("/user/login")
public SysResult login(@RequestBody User user){
//AuthenticationManager authenticate 进行用户认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());//将用户名账号密码信息封装成authentication对象
Authentication authenticate = authenticationManager.authenticate(authenticationToken);//此处会调用UserDetailServiceImpl中的方法进行账号密码校验
//如果没通过,给出对应提示
if (Objects.isNull(authenticate)){
throw new RuntimeException("登录失败");
}
//如果通过,使用userid生成一个jwt jwt存入返回
LoginUser loginUser = (LoginUser)authenticate.getPrincipal();//Principal中获得了用户的所有数据
Long userId = loginUser.getUser().getId();
String jwt = JwtUtil.createJWT(userId.toString());
HashMap<String, String> map = new HashMap<>();
map.put("token",jwt);
//把完整的用户信息存入redis中,userid作为key
redisCache.setCacheObject("login:"+userId,loginUser);
//登录成功返回token
return SysResult.success(map);
}
}
退出接口
退出时,只需将存入用户信息的数据都删除即可。
/**
* 退出
* @return
*/
@RequestMapping("/user/logout")
public SysResult logout(){
//获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();//会获取到用户的信息
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String userId = loginUser.getUser().getId().toString();
//删除redis中的值
redisCache.deleteObject("login:"+userId);
return SysResult.success("注销成功");
}
权限配置?
在用户登录时我们就已经获取到用户的权限信息了,现在要做的就是进行配置权限
1.使用注解的形式
@PreAuthorize("hasAuthority('system:test:list')")? //单个
@PreAuthorize("hasAnyAuthority('admin','test','system:test:lisy')") //多个,且只要有一个就可通过
在接口上添加Security提供的权限校验的注解。并调用其中的方法
2.在SecurityConfig中配置路径权限
.antMatchers("/testCors").hasAuthority("system:dept:list222")
3.自定义配置权限方法(功能更强大,可以自己定义权限校验形式条件等等)
import com.jt.pojo.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 自定义权限配置
*/
@Component("ex")
public class SGExpressionRoot {
public boolean hasAuthority(String authority){
//获取当前用户的权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
//判断用户权限集合中是否存在authority
return permissions.contains(authority);
}
}
?@PreAuthorize("@ex.hasAuthority('system:test:list')")
注解里面再使用注解的形式进行调用自己自定义的权限校验方法。
测试?
1.创建HelloController进行测试
import com.jt.mapper.UserMapper;
import com.jt.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 测试过滤器,权限等
*/
@RestController
public class HelloController {
@Autowired
private UserMapper userMapper;
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('system:test:list22')")
public String hello(){
List<User> users = userMapper.selectList(null);
System.out.println("users = " + users);
return "hello";
}
}
2.使用注解的形式进行测试权限
@PreAuthorize("hasAuthority('system:test:list')")
在接口上进行添加此注解,以及注解中的方法进行设定此接口需要什么权限才能访问
?数据库中信息:
?
?使用测试工具进行测试
?在登录时会从数据库验证账号密码,并获取用户的权限。且进行了绑定
?用户带着token进行访问其他接口:
?当权限一样时则通过:
|