在之前一篇 博客 已经说明了 SpringSecurity 认证与授权的原理。这篇用来具体实现一下。
1、新建SecurityConfig 并创建认证管理器
@Bean
public AuthenticationManager authenticationManager() {
...
}
2、新建认证提供者
@Configuration
public class SystemUserPasswordAuthenticationProvider extends DaoAuthenticationProvider {
...
}
3、将认证提供者加入认证管理器
@Resource
private SystemUserPasswordAuthenticationProvider systemUserPasswordAuthenticationProvider;
@Bean
public AuthenticationManager authenticationManager() {
// 将认证提供者 放入认证管理器
return new ProviderManager(systemUserPasswordAuthenticationProvider);
}
4、书写LoginServiceImpl,注入AuthenticationManager,执行其认证方法
@Service
public class LoginServiceImpl implements LoginService {
// 拿到认证管理器
@Resource
AuthenticationManager authenticationManager;
// dto 是用户名和密码
@Override
public LoginResponseVO login(UserDTO dto) {
Authentication authenticate = authenticationManager.authenticate(new SystemUserPasswordAuthenticationToken(dto));
....
}
5、调用认证管理器的authenticate,在源码中发现,调用此方法时需要传入一个 Authentication 类型的参数,用于匹配认证提供者。同时认证提供者还需实现 suppoet方法绑定到它的 Authentication。
因此为 SystemUserPasswordAuthenticationProvider 创建一个对应的 SystemUserPasswordAuthenticationToken,其继承自 UsernamePasswordAuthenticationToken
public class SystemUserPasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
// 必须在子类的构造方法中调用父类的构造方法,用来对父类的一些变量进行初始化赋值
// 这里是对父类的 principal 和 credentials 赋值
public SystemUserPasswordAuthenticationToken(UserDTO dto) {
super(dto.getUsername(), dto.getPassword());
}
}
其次在 SystemUserPasswordAuthenticationProvider 中实现 supports 方法
@Override
public boolean supports(Class<?> authentication) {
// A.isAssignableFrom(B) 判断 一个类(B)是不是继承来自于另一个父类(A);一个接口(A)是不是实现了另外一个接口(B);或者两个类相同;
return (SystemUserPasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
继续看源码,会发现通过传入的 Authentication ,遍历 AuthenticationManager 中的所有 AuthenticationProvider 中的 supports 方法,匹配到 SystemUserPasswordAuthenticationProvider 后 调用了其 authenticate 方法。该方法默认会调用父类 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法。 可以自己实现 authenticate 方法来自定义一些规则,最后再执行super去执行父类的authenticate方法。但如果只是做用户名和密码的认证的话可以不实现。
比如,有时可能有ca认证、即传入的dto不仅仅是账号密码,这时候,new SystemUserPasswordAuthenticationToken 时会将 dto 的整体赋值给 SystemUserPasswordAuthenticationToken 父类的 principal,而 credentials 为null。直到调用到我们重写的 authenticate 时,将dto中包含的多余的信息做处理,然后将 账号密码取出来 封装一个 UsernamePasswordAuthenticationToken,然后再调用 父类的 authenticate 方法。 如下图所示:
重写的 authenticate 方法,来处理 dto 中 ca信息,ca登录成功的话就不往后认证了
6、我采取只接受账号和密码,因此未重写 authenticate。继续跟着源码走,执行父类 的 authenticate 方法中,会执行 retrieveUser(username, authentiacation) 方法,进而会执行一个 loadUserByUsername(username) 方法,这个方法是用来在数据库根据用户名查找到用户信息。因此需要我们实现。分析源码发现,每个认证提供者的父类都包含一个 UserDetailsService 类型的属性,它是一个接口,实现了 loadUserByUsername 方法。因此我们需要创建一个类SystemUserDetailServiceImpl 实现 UserDetailsService 接口,并重写 loadUserByUsername 方法,然后添加到 SystemUserPasswordAuthenticationProvider 中。由于 loadUserByUsername 返回类型为 UserDetails,因此还需创建一个类 SystemUserDetail 实现 UserDetails 接口
SystemUserDetailServiceImpl :
@Service(value = "systemUserDetailService")
public class SystemUserDetailServiceImpl implements UserDetailsService {
@Resource
private SysUserMapper sysUserMapper;
// 由于返回的是一个 UserDetails 对象,因此还需新建一个继承自 UserDetails 的类
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserMapper.selectOne(Wrappers.lambdaQuery(SysUser.class).eq(SysUser::getUserName, username));
Optional.ofNullable(user).orElseThrow(() -> new BadCredentialsException("用户名或密码错误"));
return new SystemUserDetail(user, user.getPassword());
}
}
SystemUserDetail:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SystemUserDetail implements UserDetails {
// 自定义,封装个SysUser对象 和 密码
private SysUser sysUser;
private String password;
// 实现 UserDetails 接口,必须实现下面这些方法。
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return sysUser.getUserName();
}
// 这些方法是用来检查查询到的 User 对象的,都设为 true。
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// 自定义一个从上下文中获取当前用户的方法
public static SysUser getCurrentSystemUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getPrincipal().equals("anonymous")) {
throw new BadCredentialsException("当前为匿名登陆,无法访问");
}
// 在源码中分析到,最后返回的Authentication 其 principal 属性返回的是一个 UserDetails对象,我们直接可返回 SystemUserDetail
SystemUserDetail principal = (SystemUserDetail) authentication.getPrincipal();
SysUser systemUser = principal.getSysUser();
if (ObjectUtils.isNull(systemUser)) {
throw new BadCredentialsException("登陆状态失效,请重新登陆");
}
return systemUser;
}
}
将 SystemUserPasswordAuthenticationProvider 与 SystemUserDetailServiceImpl 绑定在一起:
UserDetailService 作为认证提供者父类的一个属性。可直接在 SystemUserPasswordAuthenticationProvider 的构造方法中设置,这样的话当SecurityConfig中注入 SystemUserPasswordAuthenticationProvider 时,会自动创建实例,绑定到 SystemUserDetailServiceImpl。
public SystemUserPasswordAuthenticationProvider(@Qualifier("systemUserDetailService") UserDetailsService userDetailsService) {
setUserDetailsService(userDetailsService);
}
7、继续看源码发现,获取到数据库中用户信息后,接下来 会调用check方法去检查用户,也就是调用 UserDetails 中那些必须重写的方法。检查完之后会调用 additionalAuthenticationChecks(userDetails, authentication)
userDetails 是我们调用自己重写的 loadUserByUsername方法返回的重写的 SystemUserDetail 对象,也就是从数据库中查询到的数据,authentication是包含我们dto传递的用户名和密码。这个方法就是来检查密码是否匹配的。我们可以进行重写来自己检查。因此在 SystemUserPasswordAuthenticationProvider 中实现此方法
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if(!StringUtils.equals(userDetails.getPassword(),(String) authentication.getCredentials())) {
throw new BadCredentialsException("认证失败,账号或密码错误");
} else {
logger.info("登陆成功");
}
}
8、继续看源码的话最后会发现,认证成功后会执行 createSuccessAuthentication 方法,并最终返回一个 UsernamePasswordAuthenticationToken 对象,其 principal 是一个 UserDetails 类型,在我这里就是 SystemUserDetail 对象,其 credentials 就是用户的密码。这个方法不需要实现也可以。
9、认证完成之后,将认证信息存入 security 上下文
SecurityContextHolder.getContext().setAuthentication(authenticate);
// 获取当前登陆的user对象
SysUser currentSystemUser = SystemUserDetail.getCurrentSystemUser();
10、将当前登录的用户的对象存入redis,并设置过期时间,然后签发token。
存入redis的形式是:
cacheKey : SysUser
uuid : username
public LoginResponseVO login(UserDTO dto) {
/*
调用认证管理器中的认证方法,此认证方法会根据传入的参数,匹配到认证管理器中的某一认证提供者,然后调用认证提供者的authenticate方法
传入的参数是一个 UsernamePasswordAuthenticationToken 类型,包含 principle credentials 两个参数 可以理解为 账户和密码
传入的 UsernamePasswordAuthenticationToken 这个类型主要用于匹配认证提供者。每个认证提供者都与一个特定的 UsernamePasswordAuthenticationToken 类型绑定。
认证管理器调用的authenticate方法中会调用每个认证提供者的supports方法,在这个方法中会判断认证提供者绑定的 UsernamePasswordAuthenticationToken类型 与传入的 UsernamePasswordAuthenticationToken类型是否匹配,如果匹配,就找到了此次登录的认证提供者!
*/
Authentication authenticate = authenticationManager.authenticate(new SystemUserPasswordAuthenticationToken(dto));
// 将认证信息存入 security 上下文
SecurityContextHolder.getContext().setAuthentication(authenticate);
// 获取当前登陆的user对象
SysUser currentSystemUser = SystemUserDetail.getCurrentSystemUser();
// 缓存中的key: "TOKEN:" + username + snowid。这个key会写入到签发的token中,以便于后续由token就可以在缓存中找到用户对象
String cacheKeyToken = getCacheKeyToken(currentSystemUser.getUserName(), String.valueOf(IdGeneratorUtil.snowflakeId()));
// 将当前登陆的用户对象存入redis,并设置过期时间
RedisUtil.getRedisService().set(cacheKeyToken, currentSystemUser, JwtTokenUtils.EXPIRE_TIME);
// 生成临时校验code,key 格式为"AUTH:CODE:" + uuid,username作为value,存入redis
String uuid = IdGeneratorUtil.simpleUUID();
RedisUtil.getRedisService().set(formatTempAuthCode(uuid), currentSystemUser.getUserName(), JwtTokenUtils.EXPIRE_TIME);
// 现在是根据User对象的用户名、缓存中的key,签发token
return new LoginResponseVO(JwtTokenUtils.generateManageToken(currentSystemUser, cacheKeyToken), uuid);
}
// 获取用户信息 在缓存中的 key
public static String getCacheKeyToken(String userName, String uuid) {
String cacheKey = "TOKEN:%s:%s";
return String.format(cacheKey, userName, uuid);
}
// 获取临时授权码 在缓存中的 key
public static String formatTempAuthCode(String snowid) {
String cacheKey = "AUTH:CODE:%s";
return String.format(cacheKey, snowid);
}
这里引入了工具类 JwtTokenUtils,这个类封装了一些与Token相关的方法:
- 由给出的用户信息和缓存key信息生成token
- 解析Token为Claims对象
- 从请求中获取token
还有一些属性字段:
- token名称
- 密钥
- 过期时间
public class JwtTokenUtils implements Serializable {
private static final long serialVersionUID = 1L;
// request中token的名字
private static final String TOKEN_NAME = "token";
/**
* 密钥,生成token是用密钥加密,解析数据时用密钥解密
*/
private static final String SECRET = "lmhlmh";
/**
* 过期时间,用户信息放入redis的过期时间。也可以理解为是token过期时间,因为当redis中存的用户信息过期了,token验证也通不过了,需要用户重新登陆!
*/
public static final long EXPIRE_TIME = 24 * 60 * 60;
// 下面这两个方法用于生成token
public static String generateToken(Map<String, Object> claims) {
return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, SECRET).compact();
}
// 将想存的信息存入token,必须包含的是 username 和 cacheKey
// username 用于直接比对上下文登录对象
// cacheKey 用于去缓存中拿对象,并判断登录是否已过期
public static String generateManageToken(SysUser systemUser, String cacheKey) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", systemUser.getUserName());
claims.put("email", systemUser.getEmail());
claims.put("phonenumber", systemUser.getPhonenumber());
claims.put("sex", systemUser.getSex());
claims.put("cacheKey", cacheKey);
String token = JwtTokenUtils.generateToken(claims);
return token;
}
public static Claims getClaimsFromToken(String token) {
Claims claims;
try {
// 在签发token的时候 是用 cacheKey 和 username,这里就是拿到这两个键值
claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
System.out.println(claims); // {cacheKey=TOKEN:lmh:1601582677075558400, username=lmh}
} catch (Exception e) {
claims = null;
}
return claims;
}
public static String getTokenFromRequest(HttpServletRequest request) {
// 从请求头中拿取token
String token = request.getHeader(TOKEN_NAME);
if (StringUtils.isBlank(token)) {
return null;
}
return token;
}
}
生成雪花id 和 uuid 时引入了 IdGeneratorUtil工具类:
@Component
public class IdGeneratorUtil {
private long workerId = 0;
private static Snowflake snowflake;
@PostConstruct
void init() {
snowflake = IdUtil.createSnowflake(workerId, 1);
}
/**
* 获取一个批次号,形如 2019071015301361000101237
* <p>
* 数据库使用 char(25) 存储
*
* @return 返回批次号
*/
public synchronized static String batchId(String prefix) {
String date = DateTime.now().toString(DatePattern.PURE_DATETIME_PATTERN);
return prefix + date;
}
/**
* 生成的是不带-的字符串,类似于:b17f24ff026d40949c85a24f4f375d42
*
* @return
*/
public static String simpleUUID() {
return IdUtil.simpleUUID();
}
/**
* 生成的UUID是带-的字符串,类似于:a5c8a5e8-df2b-4706-bea4-08d0939410e3
*
* @return
*/
public static String randomUUID() {
return IdUtil.randomUUID();
}
public static synchronized long snowflakeId() {
return snowflake.nextId();
}
public synchronized long snowflakeId(long workerId, long dataCenterId) {
Snowflake snowflake = IdUtil.createSnowflake(workerId, dataCenterId);
return snowflake.nextId();
}
}
11、这里说一下 Redis 的配置:
(1)增添 redis 依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
(2)application.xml 文件
配置 redis 连接信息。
spring:
redis:
port: 6379
database: 0 # 指定数据库编号
host: master
timeout: 10000
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
(3)RedisConfig 文件
自定义 Redistemplate,并装配到Spring容器中管理。
这里一般是固定的模板:
@EnableCaching
@Configuration
@ConditionalOnClass(RedisConnectionFactory.class)
public class RedisConfig {
@Bean(value = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用 GenericFastJsonRedisSerializer 替换默认序列化
GenericFastJsonRedisSerializer genericFastJsonRedisSerializer = new GenericFastJsonRedisSerializer();
// 设置key和value的序列化规则
redisTemplate.setKeySerializer(new GenericToStringSerializer<>(Object.class));
redisTemplate.setValueSerializer(genericFastJsonRedisSerializer);
// 设置hashKey和hashValue的序列化规则
redisTemplate.setHashKeySerializer(new GenericToStringSerializer<>(Object.class));
redisTemplate.setHashValueSerializer(genericFastJsonRedisSerializer);
// 设置支持事物
//redisTemplate.setEnableTransactionSupport(true);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
(4)编写RedisService接口和实现类:
实现类中注入自定义的 RedisTemplate,实现redis中常用的各种方法,然后将实现类交给 Spring 容器中。
public interface RedisService {
/**
* 保存属性
*/
void set(String key, Object value, long time);
void set(String key, Object value, long time, TimeUnit timeUnit);
/**
* redis 原子类自增
*
* @param key key
* @param time 时间
* @param timeUnit 单位
* @return 自增后的值
*/
long increment(String key, long time, TimeUnit timeUnit);
/**
* 保存属性
*/
void set(String key, Object value);
/**
* 获取属性
*/
Object get(String key);
/**
* 删除属性
*/
Boolean del(String key);
/**
* 批量删除属性
*/
Long del(List<String> keys);
/**
* 设置过期时间
*/
Boolean expire(String key, long time);
/**
* 设置过期时间
*/
Boolean expire(String key, long time, TimeUnit timeUnit);
/**
* 获取过期时间
*/
Long getExpire(String key);
/**
* 判断是否有该属性
*/
Boolean hasKey(String key);
/**
* 按delta递增
*/
Long incr(String key, long delta);
/**
* 按delta递减
*/
Long decr(String key, long delta);
/**
* 获取Hash结构中的属性
*/
Object hGet(String key, String hashKey);
/**
* 向Hash结构中放入一个属性
*/
Boolean hSet(String key, String hashKey, Object value, long time);
/**
* 向Hash结构中放入一个属性
*/
void hSet(String key, String hashKey, Object value);
/**
* 直接获取整个Hash结构
*/
Map<Object, Object> hGetAll(String key);
/**
* 直接设置整个Hash结构
*/
Boolean hSetAll(String key, Map<String, Object> map, long time);
/**
* 直接设置整个Hash结构
*/
void hSetAll(String key, Map<String, Object> map);
/**
* 删除Hash结构中的属性
*/
void hDel(String key, Object... hashKey);
/**
* 判断Hash结构中是否有该属性
*/
Boolean hHasKey(String key, String hashKey);
/**
* Hash结构中属性递增
*/
Long hIncr(String key, String hashKey, Long delta);
/**
* Hash结构中属性递减
*/
Long hDecr(String key, String hashKey, Long delta);
/**
* 获取Set结构
*/
Set<Object> sMembers(String key);
/**
* 向Set结构中添加属性
*/
Long sAdd(String key, Object... values);
/**
* 向Set结构中添加属性
*/
Long sAdd(String key, long time, Object... values);
/**
* 是否为Set中的属性
*/
Boolean sIsMember(String key, Object value);
/**
* 获取Set结构的长度
*/
Long sSize(String key);
/**
* 删除Set结构中的属性
*/
Long sRemove(String key, Object... values);
/**
* 获取List结构中的属性
*/
List<Object> lRange(String key, long start, long end);
/**
* 获取List结构的长度
*/
Long lSize(String key);
/**
* 根据索引获取List中的属性
*/
Object lIndex(String key, long index);
/**
* 向List结构中添加属性
*/
Long lPush(String key, Object value);
/**
* 向List结构中添加属性
*/
Long lPush(String key, Object value, long time);
/**
* 向List结构中批量添加属性
*/
Long lPushAll(String key, Object... values);
/**
* 向List结构中批量添加属性
*/
Long lPushAll(String key, Long time, Object... values);
/**
* 从List结构中移除属性
*/
Long lRemove(String key, long count, Object value);
}
@Service("redisService")
public class RedisServiceImpl implements RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void set(String key, Object value, long time) {
set(key, value, time, TimeUnit.SECONDS);
}
@Override
public void set(String key, Object value, long time, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, time, timeUnit);
}
@Override
public long increment(String key, long time, TimeUnit timeUnit) {
RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
redisAtomicLong.expire(time, timeUnit);
return redisAtomicLong.incrementAndGet();
}
@Override
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
@Override
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
@Override
public Boolean del(String key) {
return redisTemplate.delete(key);
}
@Override
public Long del(List<String> keys) {
return redisTemplate.delete(keys);
}
@Override
public Boolean expire(String key, long time) {
return redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
@Override
public Boolean expire(String key, long time, TimeUnit timeUnit) {
return redisTemplate.expire(key, time, timeUnit);
}
@Override
public Long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
@Override
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
@Override
public Long incr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
@Override
public Long decr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, -delta);
}
@Override
public Object hGet(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
@Override
public Boolean hSet(String key, String hashKey, Object value, long time) {
redisTemplate.opsForHash().put(key, hashKey, value);
return expire(key, time);
}
@Override
public void hSet(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
@Override
public Map<Object, Object> hGetAll(String key) {
return redisTemplate.opsForHash().entries(key);
}
@Override
public Boolean hSetAll(String key, Map<String, Object> map, long time) {
redisTemplate.opsForHash().putAll(key, map);
return expire(key, time);
}
@Override
public void hSetAll(String key, Map<String, Object> map) {
redisTemplate.opsForHash().putAll(key, map);
}
@Override
public void hDel(String key, Object... hashKey) {
redisTemplate.opsForHash().delete(key, hashKey);
}
@Override
public Boolean hHasKey(String key, String hashKey) {
return redisTemplate.opsForHash().hasKey(key, hashKey);
}
@Override
public Long hIncr(String key, String hashKey, Long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, delta);
}
@Override
public Long hDecr(String key, String hashKey, Long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, -delta);
}
@Override
public Set<Object> sMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
@Override
public Long sAdd(String key, Object... values) {
return redisTemplate.opsForSet().add(key, values);
}
@Override
public Long sAdd(String key, long time, Object... values) {
Long count = redisTemplate.opsForSet().add(key, values);
expire(key, time);
return count;
}
@Override
public Boolean sIsMember(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
@Override
public Long sSize(String key) {
return redisTemplate.opsForSet().size(key);
}
@Override
public Long sRemove(String key, Object... values) {
return redisTemplate.opsForSet().remove(key, values);
}
@Override
public List<Object> lRange(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
@Override
public Long lSize(String key) {
return redisTemplate.opsForList().size(key);
}
@Override
public Object lIndex(String key, long index) {
return redisTemplate.opsForList().index(key, index);
}
@Override
public Long lPush(String key, Object value) {
return redisTemplate.opsForList().rightPush(key, value);
}
@Override
public Long lPush(String key, Object value, long time) {
Long index = redisTemplate.opsForList().rightPush(key, value);
expire(key, time);
return index;
}
@Override
public Long lPushAll(String key, Object... values) {
return redisTemplate.opsForList().rightPushAll(key, values);
}
@Override
public Long lPushAll(String key, Long time, Object... values) {
Long count = redisTemplate.opsForList().rightPushAll(key, values);
expire(key, time);
return count;
}
@Override
public Long lRemove(String key, long count, Object value) {
return redisTemplate.opsForList().remove(key, count, value);
}
}
(5)其实此时已经可以用了,哪里用的话,直接注入 redisService 就可以了。
但再封装一个 RedisUtils 用来拿到容器中的 redisService,这样的话在别处用的时候直接 RedisUtil.getRedisService(). 就可以了。
@Component
public class RedisUtil {
public static RedisService getRedisService() {
return Inner.REDIS_SERVICE;
}
private static class Inner {
private static final RedisService REDIS_SERVICE = (RedisService) MyProjectApplication.ac.getBean("redisService");
}
}
/*
这里的 MyProjectApplication 是我的启动类,ac是我在启动类里创建的一个实例,
用来获取到ApplicationContext对象,从而再调用getBean方法。
*/
注意: 这里是在静态类里通过 MyProjectApplication.ac.getBean 来拿到Spring容器中的 redisService 对象。这里不可以采取注入的方式,因为 getRedisService 必须是一个静态方法来让外部去调用,而通过 @Resource 或 @Autowired 注入的对象不能是静态的,因为在项目启动时,会加载所有带有类似@Component等注解的类,并加载该类下的所有静态属性。也就是说,如果 @Autowired static redisService 这种形式的话,会报空指针错误,也就是会拿不到,因为可能redisService类还没加载呢,这里的静态属性先加载了。但是在类加载时,它里面的静态内部类不会随着加载,而是会在第一次被调用时加载,因此将获取bean的方法放在内部类里,就可以保证在getBean时,redisService已经被加载完毕了(redisService是另一个类装配到Spring容器中的)。
还需注意: 在 java中,若想在另一个类直接通过类名访问到本类的方法,方法必须是静态的。并且静态方法中不能包括非静态字段。如果返回某个字段时,该字段有也必须是静态的。
12、回到 SecurityConfig,开始配置对请求的身份验证。
编写一个 securityFilterChain 方法,接收 HttpSecurity 参数,返回 SecurityFilterChain 然后装配到 Spring 容器。
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
// 放行的路径,即这些请求不需要验证
for (String url : ignoreUrlConfig.getIgnoreUrlList()) {
registry.antMatchers(url).permitAll();
}
return httpSecurity.cors().and().csrf().disable()
.authorizeRequests() // 请求授权
.anyRequest() // 任何请求
.authenticated() // 都必须经过身份验证
.and()
// 自定义权限拦截器 JWT过滤器
.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling().authenticationEntryPoint(noAuthenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.headers().cacheControl()
.and()
.and()
.build();
}
针对上面用到的
(1)放行路径
在 application.yml 中指明放行路径
secure:
ignoreUrlList:
- /login/**
- /doc.html
- /swagger-resources/**
- /v3/api-docs
- /swagger*/**
- /webjars/**
新建一个类用来拿到yml文件中的配置,然后装配到Spring容器。
@Data
@Configuration
@ConfigurationProperties(prefix = "secure")
public class IgnoreUrlConfig {
private List<String> ignoreUrlList;
}
然后在SecurityConfig中注入进来,并设置这些url permitAll();
@Resource
private IgnoreUrlConfig ignoreUrlConfig;
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
// 放行的路径
for (String url : ignoreUrlConfig.getIgnoreUrlList()) {
registry.antMatchers(url).permitAll();
}