SpringSecurity框架
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.1port: 6379datasource:#高版本驱动使用driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true#设定用户名和密码username: rootpassword: 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;/*** 主键*/@TableIdprivate 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;@TableIdprivate 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
{@Autowiredpublic 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;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true; //更改成了true}@Overridepublic boolean isAccountNonLocked() {return true; //更改成了true}@Overridepublic boolean isCredentialsNonExpired() {return true; //更改成了true}@Overridepublic 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 {@Autowiredprivate UserMapper userMapper;@Autowiredprivate 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">SELECTDISTINCT m.`perms`FROMsys_user_role urLEFT 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`WHEREuser_id = #{userId}AND r.`status` = 0AND m.`status` = 0</select>
</mapper>
三:SecurityConfig配置
·注入BC加密对象
//创建BCryptPasswordEncoder 注入容器 加密方式@Beanpublic PasswordEncoder passwordEncoder(){//当你将此对象注入容器时,就会自动将密码进行bc的比对校验。//如果输入的明文密码与数据库中的加密密码不匹配则报错。//切数据库中必须存储为bc加密的密码return new BCryptPasswordEncoder();}
测试BC加密,BC校验
bc是一个强大的APi对象,主要核心包含俩个功能。1.进行加密 2.校验用户输入的密码与数据库中存入的密文是否一致
存入数据库中的密码应该是加密的密码。
@Testpublic 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");//如果密码与输入的相同则返回 trueSystem.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 {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取tokenString token = request.getHeader("token");if (StringUtils.isEmpty(token)){//如果为空//放行 因为后面还有其他的过滤器会进行判断并提示filterChain.doFilter(request,response);return;}//解析tokenString 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 {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping("/**")// 设置允许跨域请求的域名.allowedOriginPatterns("*")// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods("GET", "POST", "DELETE", "PUT")// 设置允许的header属性.allowedHeaders("*")// 跨域允许时间.maxAge(3600);}
}
第二种:
package com.jt.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;/*** 解决跨域请求的*/@Configuration
public class CorsConfig {private CorsConfiguration buildConfig() {CorsConfiguration corsConfiguration = new CorsConfiguration();// 你需要跨域的地址 注意这里的 127.0.0.1 != localhost// * 表示对所有的地址都可以访问corsConfiguration.addAllowedOrigin("*");// 表示只允许http://localhost:8080地址的访问(重点哦!!!!)// corsConfiguration.addAllowedOrigin("http://localhost:8080");// 跨域的请求头corsConfiguration.addAllowedHeader("*"); // 2// 跨域的请求方法corsConfiguration.addAllowedMethod("*"); // 3//加上了这一句,大致意思是可以携带 cookie//最终的结果是可以 在跨域请求的时候获取同一个 sessioncorsConfiguration.setAllowCredentials(true);return corsConfiguration;}@Beanpublic CorsFilter corsFilter() {UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();//配置 可以访问的地址source.registerCorsConfiguration("/**", buildConfig()); // 4return new CorsFilter(source);}
}
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 {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Autowiredprivate AccessDeniedHandlerImpl accessDeniedHandler;@Autowiredprivate AuthenticationEntryPointImpl authenticationEntryPoint;//创建BCryptPasswordEncoder 注入容器 加密方式@Beanpublic 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 {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate 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作为keyredisCache.setCacheObject("login:"+userId,loginUser);//登录成功返回tokenreturn SysResult.success(map);}}
退出接口
退出时,只需将存入用户信息的数据都删除即可。
/*** 退出* @return*/@RequestMapping("/user/logout")public SysResult logout(){//获取SecurityContextHolder中的用户idUsernamePasswordAuthenticationToken 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();//判断用户权限集合中是否存在authorityreturn 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 {@Autowiredprivate 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进行访问其他接口:
当权限一样时则通过:
SpringSecurity框架相关推荐
- 前后端分离开发下的权限管控 :SpringSecurity 框架
首先在了解前后端分离模式下使用SpringSecurity框架之前,我们需要先了解token和jwt(Json Web Token)技术 token 和 session 的区别? 由于http协议是无 ...
- Spring Security(一)- SpringSecurity 框架简介
文章目录 一.SpringSecurity 框架简介 1. 概要 2. Spring Security 与 Shiro 对比 2.1 Spring Security 2.2 SpringSecurit ...
- 使用SpringBoot框架和SpringSecurity框架整合出现because its MIME type ('text/html') is not executable
前端页面出现: Refused to execute script from 'http://localhost:8080/login' because its MIME type ('text/ht ...
- 手把手带你撸一把springsecurity框架源码中的认证流程
提springsecurity之前,不得不说一下另外一个轻量级的安全框架Shiro,在springboot未出世之前,Shiro可谓是颇有统一J2EE的安全领域的趋势. 有关shiro的技术点 1.s ...
- [SpringSecurity]框架概述
概要 Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的 成员.Spring Security 基于 Spring 框架,提供了一套 ...
- SpringSecurity框架【详解】
SpringSecurity 来源视频 文章目录 SpringSecurity 1.概述 2.Spring Security.Apache Shiro 选择问题 2.1.Shiro 2.1.1.shi ...
- SpringBoot2.0 整合 SpringSecurity 框架,实现用户权限安全管理
一.Security简介 1.基础概念 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配 ...
- 翼支付门户架构之搭建spring+springmvc+springsecurity框架
1.项目结构如下: 2.pom文件的依赖配置如下: <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi= ...
- 根据Spring-Security安全框架搭建问答论坛系统(更新中.....)
论坛问答系统系统设计与实现 什么是Spring安全框架 为什么需要Spring-Security 启动Spring-Security 访问控制器方法 密码加密 Spring-Security的权限管理 ...
最新文章
- java 输入流可以合并吗_HOW2J Java 文件输入输出流,合并与拆分
- 多变异位自适应遗传算法(MMAdapGA)的算法原理、算法步骤和matlab实现
- Nodejs简介以及Windows上安装Nodejs
- Java多线程之龟兔赛跑和抢票
- IDEA导入项目笔记二
- 在Java中避免NullPointerException
- android读取mysql数据库文件_Android开发系列(十七):读取assets目录下的数据库文件...
- C++基础教程,基本的输入输出
- hdu acmsteps 2.1.3 Cake
- 使用radioGroup的时候,每个radioButton的状态选择器要使用 state_checked=属性,不能使用selected...
- 2021-11-01 讲题题解
- android 头像球_Android自定义View实现圆形头像效果
- 在 TensorFlow 上使用 LSTM 进行情感分析
- JS中几种绑定事件的方式
- Spring Bean到底是什么?有什么用?
- 如何用canvas实现五子棋
- oracle mapviewer 11g安装使用,Oracle MapViewer11g安装与部署
- Oracle JDE MRP FPO目的、设置与使用
- LCD断码屏显示应用框架
- 【蜂鸟E203内核解析】Chap.2 E203内核中指令执行的过程-为什么E203是两级流水线?