SpringSecurity
主要是 过滤器 和 拦截器
不是功能性需求
主要就是 认证 和 授权
SpringSecurity
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
进行设置,后面会默认跳到login.html页面,这是内置的页面
2. 认证的主要流程
UsernamePasswordAuthenticationFilter : 负责处理我们再登陆页面填写了用户名、密码后的登陆请求。认证工作主要是它负责。 认证过程
ExceptionTranslationFilter : 处理过滤器中抛出的任何AccessDeniedException 和 AuthenticationException 异常处理过程
FilterSecurityInterceptor : 负责权限校验的过滤器 授权处理过程
认证流程
一般来说,对于第一Filter来讲,实际处理中需要返回数据token,所以可以使用一个controller进行处理,调用ProviderManager
调用过程中,怎么判断 请求携带了token,使用JWT 过滤器
在认证token 过后,怎么获取用户信息,而不频繁调用数据库, 使用redis内存访问,减少硬盘负担,挺高效率
相关设置
redis 进行序列化操作,防止乱码出现
package com.pengshi.chartroom.controller;
import com.pengshi.chartroom.utils.FastJson2JsonRedisSerializer;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
FastJson2JsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJson2JsonRedisSerializer<>(Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
package com.pengshi.chartroom.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.util.Assert;
import java.nio.charset.Charset;
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
private ObjectMapper objectMapper = new ObjectMapper();
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJson2JsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
}
protected JavaType getJavaType(Class<?> clazz) {
return TypeFactory.defaultInstance().constructType(clazz);
}
}
前后端分离项目,一般都会统一一下响应的格式
JWT 结构
什么是 JWT – JSON WEB TOKEN - 简书 (jianshu.com)
令牌组成
- 标头 Header
- 有效载荷 Payload
- 签名 Signature
形式 为 xxxxxx.yyyyyy.zzzzz
-
Header 使用的签名算法 HMAC SHA256 或者 RSA,使用base64 编码组成JWT结构 {
"alg" : "HS256",
"typ" : "JWT"
}
推荐使用HS256 -
Payload 有效负载,包含声明,使用Base64 编码 {
"sub" : "1234567890",
"name" : "pengchuang",
"admin" : true
}
但是这一部分的信息,不建议放重要信息,这块信息是对称加密,截获可能被获取信息 -
Signature 就是签名 基于前两个的base64的编码,加上随机盐,也就是签名密钥,就是查看前两个信息有没有被改过,通过第三部分的签名进行验证
实现
首先将相关代码打出,其中redis的工具类和jwt的工具类,网上一大把,同时前后端交互的pojo类吧,其实就是显示状态码和系统信息的,作为前后端交互的类,也要打出。
现在从数据访问入手,对于security访问数据进行用户信息的读取,便于验证进行设计。
对于常见业务的建表语句
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 '0' comment '邮箱',
`phonenumber` varchar(11) default NULL comment '头像',
`sex` char(1) not null default '1' 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 = 2 DEFAULT CHAR SET = utf8mb4 comment '用户表'
对于实现第四个步骤,调取数据库的身份信息进行验证的过程中,需要将用户信息打包成UserDetails
@TableName(value = "sys_user")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable, UserDetails {
@TableId
private Long id;
@TableField(value = "user_name")
private String name;
private String nick_name;
@TableField(value = "password")
private String pwd;
private String status;
private String email;
private String phonenumber;
private String sex;
private String avatar;
private String user_type;
private String create_by;
private String create_time;
private String update_by;
private String update_time;
private Integer del_flag;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
直接将user类实现UserDetails的功能
由于springsecurity的安全性,密码都会经过passwordencoding进行加密解密,而对于明文存储的密码,使用{noop}表示明文的存储
对于认证过程中数据库调用的security实现类如下
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getName, username);
User user = userMapper.selectOne(queryWrapper);
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误!!!");
}
return user;
}
}
- 密码加密的存储,实际过程中,密码明文不会存储在数据库中的,所以我们需要加上一个密码校验其PasswordEncoder,一般使用的是SpringSecurity为我们提供的BCryptPasswordEncoder,这个类的注入要继承WebSecurityConfigureAdapter
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
bCryptPasswordEncoder.encode();
bCryptPasswordEncoder.matches();
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String str = bCryptPasswordEncoder.encode("1234");
System.out.println(str);
System.out.println(bCryptPasswordEncoder.matches("1234", str));
JWT 工具类
import com.pengshi.chartroom.pojo.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class JWTUtils {
private static final String CLAIM_KEY_USERNAME="sub";
private static final String CLAIM_KEY_CREATED="created";
@Value("${jwt.secret}")
private String secret = "good";
@Value("${jwt.expiration}")
private Long expiration = 1000L;
private String generateToken(Map<String, Object> claims) {
System.out.println(secret);
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
Date expireDate = getExpiredDateFromToken(token);
return expireDate.before(new Date());
}
public boolean canRefresh (String token) {
return !isTokenExpired(token);
}
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
public Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
System.out.println("转译有问题");
e.printStackTrace();
}
return claims;
}
public Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
public static void main(String[] args) {
User user = new User();
user.setName("pengshi");
JWTUtils jwtUtils = new JWTUtils();
System.out.println(jwtUtils.generateToken(user));
}
}
登陆接口
自定义登陆接口,其实就是Spring中的AuthenticationManager 就是上面架构图中,时序图的第二部,主要是管理页面的访问功能,加上发送认证请求给后面,其中对于访问页面的控制中,其实可以添加我们自己自定义的页面,同时放行一些页面。
在这里可以卡一个jwt认证过滤器,对于已经认证的请求,可以放行给token过去,毕竟jwt也是无状态的
对于端口的暴露,使用的是WebSecurityConfigurationAdapter中的authenticationManagerBean方法,进行对于authenticationManager的设计
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/user/login").anonymous()
.anyRequest().authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
这是认证对象的组成信息,其中getPrincipal就是返回的User对象,我们需要理解的是,authenticationManager,通过authenticate方法 调用到时序图的第四层进行数据库中用户信息的认证和获取,返回还是一个Authentication对象,但是这个对象的信息是对于真正的数据库信息来说的,这里面的信息就是数据库中的信息。而像下面的UsernamePasswordAuthenticationToken 发送到后面 时序图DAO层中的进行拆解,拿到账号和密码,可以看到下面的过程也是对于账号密码的封装发送到DAO层中,DAO层中拆解,拿到信息,后面DetailsService层中 UserDetailsService ,调用loadUserByUsername 加上上层DAO获得的username进行数据库信息查询,返回UserDatils信息回去,这里我将User实现UserDatils接口,将User类作为一个通用的认证数据
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
user.getName(), user.getPwd());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (authenticate == null) {
throw new RuntimeException("登陆失败!!!");
}
User user1 = (User) authenticate.getPrincipal();
Long id = user1.getId();
String token = JWTUtils.generateToken(user1);
redisCache.setCacheObject("login:"+user1.getId(), user1);
Map<String, String> hashMap = new HashMap();
hashMap.put("token", token);
return new ResponseResult(200, "登陆成功", hashMap);
}
}
Redis 作为用户信息的存储
使用redis是防止对于数据库中,用户调取信息的频繁访问,使用redis,因为内存可以快速访问,主还是jwt是无状态的
redis配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
}
properties的配置
spring.redis.timeout=10000ms
spring.redis.host=39.108.94.255
spring.redis.port=6379
spring.redis.database=0
spring.redis.password=root
spring.redis.lettuce.pool.max-active=1024
spring.redis.lettuce.pool.max-wait=10000ms
spring.redis.lettuce.pool.max-idle=200
spring.redis.lettuce.pool.min-idle=5
JWT 过滤器
我们使用的是springmvc提供的OncePerRequestFilter的类,继承这个类,主要是通过这个类进行过滤器的设计,从英语意思上来看,每次请求至少经过一个,所以使用这个过滤器更为方便
注意可以从上面的时序图中看出过滤器Filter请求后,响应的时候还是会经过过滤器的,这个jwt过滤器放在前面
注意一下对于 UsernamePasswordAuthenticationToken 解析中
- 可以知道,三个参数方法的认证中,是对于认证通过的标志,两个参数为false
- 使用三个参数的方法,可以卡在token验证jwt过滤器上面,放行后面的拦截器,验证通过
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
- principal 其实就是认证的对象 credentials就是认证的凭证,一般来说,假如还没有验证setAuthenticated(false);在credentials添加密码,这个密码会自动和PasswordEncoding 和 后端得出的DatilsService上的Password进行两者转换验证,认证通过setAuthenticated(true);
如何添加过滤器在拦截器之前呢,也就是对于jwt过滤器而言,要在拦截器之前进行过滤认证?
-
使用 WebSecurityConfigurerAdapter 中 addFilterBefore 功能进行过滤器拦截,可以知道我们需要时使用UsernamePasswordAuthenticationFilter,作为设置将jwtAuthenticationTokenFilter放在UsernamePasswordAuthenticationFilter之前执行,也就是用户名和密码验证之前进行token校验,如果没有token经过这个用户密码验证,有有效的token就放行,使其畅通无阻。 -
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
-
注意如果遇到 UnrecognizedPropertyException Caused by: com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "enabled"
这种错误是因为json格式中有些属性与转化的类不一样,所以加上下面的注解进行忽略操作
@JsonIgnoreProperties(ignoreUnknown = true)
-
后面,请求操作的时候携带请求头header 里面包含token就可以进行认证成功的操作,访问相关页面
退出登录
其实就是删除 redis里面的数据和 SecurityContextHolder的数据,表明退出了
由于 /logout 这个url被spring占用默认为退出功能,所以可以在拦截器那里通过config http 关掉logout功能,使用该url, 相关配置就是下面关闭了logout功能,能够正常占用logout 连接进行退出操作
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login").anonymous()
.anyRequest().authenticated()
.and()
.logout()
.disable();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
-
antMatchers(url).permitAll() 即使登不登陆都可以访问 -
antMatchers(url).anonymous() 匿名访问,未登录下登陆 -
anyRequest().authenticated() 任意用户认证成功都可以访问 -
注意 拦截器中 ,具有Filter部署,和session管理
2. 权限 授权
判断用户认证后是否有相关权限进行访问
开启访问权限
@EnableGlobalMethodSecurity(prePostEnabled = true)
对应使用相关注解进行判断
@RestController
public class HelloController {
@PreAuthorize("hasAnyAuthority('test')")
@GetMapping("/hello")
public String hello() {
return " fdsafdsafd";
}
}
@PreAuthorize(“hasAnyAuthority(‘test’)”) 在访问之前判断是否具有这个权限
怎么录入权限呢?我们可以知道在UsernamePasswordAuthenticationToken后面的第三个参数是一个collection集合,其中对于DetilsService 中getAuthorities 也要进行改装。同时解释一下DetilsService 和 Authentication等实现类的区别。
-
大致逻辑其实就是 前端发送用户密码封装为 Authentication 等实现类,传给DAO层进行验证,DAO提取其中的用户名发送给数据库连接层UserDetilsService上,进行数据库的查询,获得用户信息。 -
由数据库获得的用户信息,被系统封装成 UserDetails 实现类,其实就是返回的查询的用户信息,再传递回给DAO层,进行密码等验证判断。 -
实现UserDetails中的实现方法 获取权限如下 @Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return null;
}
其中要知道的是 权限授予必须继承了GrantedAuthority 为了方便每次验证的时候,都不用进行转化浪费时间,可以使用全局变量保存list权限信息,在里面卡一判断,是否为空 @JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null) return authorities;
authorities= permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
这里需要注意的是权限信息,不能进行JSON流处理,要记住,不然Spring处理会报错
我们在 UserDetails loadUserByUsername 中写入权限信息,注入到 UserDetails实现类中,其中权限信息可以从数据库中获取,进行写入,同时登陆的时候一并记录在Redis中,设置UserDetails 实现类中设置一个新的List存放权限,录入这个权限后,怎么执行其中返回权限信息?使用UserDetails的实现方法进行设置,使用 getAuthorities()使得每次调用相关url时候进行权限的获取判断,而对于需要权限的url 有 @PreAuthorize("hasAnyAuthority('test')") 这个注解,可以进行判断,是否通过。
-
特别注意:对于Mybatis很智能,能够自己对应类中属性进行数据库的匹配,但是也要注意当debug 看到var6的错误的时候,应该就是类中有些属性是没有对应数据库加上注解 @TableField(exist = false) -
Caused by: com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "
-
上面错误是因为权限使用的过程中权限 json解析失败了 @JSONField(serialize = false) 使用属性注解,将这个部分不序列化,但是好像没有用,还是老老实实的写一个反序列化的类,但是写了反序列化类更没什么用处,懵逼了我,反序列化类如下 class CustomAuthorityDeserializer extends JsonDeserializer {
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) p.getCodec();
JsonNode jsonNode = mapper.readTree(p);
LinkedList<GrantedAuthority> grantedAuthorities = new LinkedList<>();
Iterator<JsonNode> elements = jsonNode.elements();
while (elements.hasNext()) {
JsonNode next = elements.next();
JsonNode authority = next.get("authority");
grantedAuthorities.add(new SimpleGrantedAuthority(authority.asText()));
}
return grantedAuthorities;
}
}
最后还是使用 注解@JsonIgnore 将对应属性进行忽略 @TableField(exist = false)
@JsonIgnore
private List<SimpleGrantedAuthority> authorities;
从数据库查询权限信息
RBRA 权限模型 (Role-Based Access Control) 即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用的权限模型
用户表-权限表-角色表
- 多个角色可以有多个权限,使用一个关联表进行设计
- 多个用户可以有多个角色,使用一个关联表进行设计
|