目录
之前的项目都是基于
session
的会话,用户的信息数据储存在
session
中;
但是有两个弊端,1.session不是很安全,2.在分布式项目中session的管理是个问题;
所以尝试使用jjwt来解决问题
由于security原生支持的是session
。所以改jwt
我们就需要手动完成框架自动完成的部分;
- 手动加入过滤器,拦截请求进行token认证;
- 手动调用登录请求调用loadUser,进行登录认证;
- 手动完成用户Conext的创建,并设进线程域对象;
至于用户信息的管理:
- 直接在载荷里存入用户信息,当然了如果权限管理比较复杂可能载荷不够装,当然如果你是基于角色的权限管理。完全可以用;
- 每次请求都通过token获取用户索引信息,查找用户信息生成权限对象设置进,如果是admin倒是无所谓,做admin的完全可以使用
- 如果你的项目是对公项目却想用security怎么办?可以尝试使用redis缓存数据库来解决问题,redis可以抗住每秒数万的并发量,如果并发量还是扛不住怎么办?直接集群安排上!大不了用户数据丢了重新登录,所以没必要设置持久化;安排一个最纯粹的缓存;需要注意的是,完成有用户权限的更新以后需要对缓存进行删除;让程序重新加载数据库真实数据;
需要准备的部分
-
实现接口重写用户加载逻辑
UserDetailsService
-
继承抽象类重写权限校验规则
WebSecurityConfigurerAdapter
- 路由权限配置
- 添加过滤器到
UsernamePasswordAuthenticationFilter
之前 - 异常处理
http.exceptionHandling()
- 注入
AuthenticationManager
的bean - 处理跨域
- 配置身份认证处理器``以及加密bean
- 注入加密bean
PasswordEncoder
-
实现接口重写用户详情对象
UserDetails
-
准备一个JwtToken工具类
-
准备一个过滤器类拦截请求进行验证
auth.userDetailsService(userDetailLoad).passwordEncoder(passwordEncoder);
-
准备一个接口校验登录信息
-
准备一个方法完成token认证以后创建用户认证对象存入线程域;(就是在这里可以采用以上用户信息管理中的任意一种完成用户数据的储存读取)
下面是代码部分
关键部分
这里创建了一个权限对象,我们把他放进线程域对象就可以实现角色权限的配置了;
/**
* 重要方法#################################################################################################################
* 传入AuthenticationToken,
* @param authenticationToken 一个解析了token后包含email和是否过期boolean的对象
* @return
*/
public Authentication getAuthentication(AuthenticationToken authenticationToken) {
//主动调用security加载用户方法进行账户验证,传入从载荷中获取的username(实际是我们的登录名称,可以是email)
//获取验证后生成的用户详情对象
UserDetails userDetails = memberDetailService.loadUserByUsername(authenticationToken.getEmail());
//传入用户信息,权限信息;返回一个包含权限集合,和用户信息的对象
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
然后我们在过滤器中将他加入线程域对象即可
SecurityContextHolder.getContext().setAuthentication(authentication);
下面是具体的解释:
哦豁,,BBQ了。。今天过来翻笔记才发现那天没保存。。
只能随手说一下大概逻辑了
就是创建一个过滤器处理jwt,在里面校验token,然后校验通过后把安全上下文对象交给上下文管理员,然后就走到下一个过滤器Usenamepassword过滤器的时候发现有上下文对象,就直接可以继续访问了,
如果不想每次都花费一部解析token的步骤[不推荐],可以在解析前看一看是否有上下文对象,有的话就步解析了
=============================================
后续
此方法为重写原来的用户认证校验过滤器,
而非自己提供接口进行账户校验;
场景:
项目完成需要附属完成一个admin
排除项目中的拦截器,security不使用原始的拦截器完成token校验
@SpringBootApplication(
scanBasePackages = {"com.d.p.admin", "com.d.p.common"}
)
@ComponentScan(excludeFilters = {
@ComponentScan.Filter( type = FilterType.REGEX,pattern = "com.doria.pzh.common.interceptor.*")
})
@MapperScan(value = {
"com.d.p.common.mapper"
})
@EnableTransactionManagement
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}
加载Jwt配置
/**
* admin jwt配置
*/
@Configuration
@PropertySource("classpath:admin-setting.properties")
@ConfigurationProperties(prefix = "admin")
@Data
public class AdminJwtConfig {
private String secretKey;
private String refreshTokenSecretKey;
private Integer tokenExpiration;
private Integer refreshTokenExpiration;
}
配置用户详情加载
实现接口UserDetailService
/**
* 加载用户详情
*/
@Component
public class UserDetailLoad implements UserDetailsService {
@Autowired
private AdminUserDao adminUserDao;
@Autowired
private AdminSysAuthDao adminSysAuthDao;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
PzhSysUser userByEmail = adminUserDao.getUserByEmail(email);
if (ObjectUtils.isEmpty(userByEmail))
return new SysUserDetail();
return SysUserDetail.builder()
.id(userByEmail.getId())
.auths(adminSysAuthDao.getAuthsByUserId(userByEmail.getId()))
.email(userByEmail.getEmail())
.password(userByEmail.getPassWord())
.ifLock(userByEmail.getLocked())
.status(0)
.build();
}
}
用户详情对象
我们需要有一个类实现UserDetails接口,实现它各种获取关键数据
的方法
大概如下,权限对象是一个GrantedAuthority
可以看到它只有一个getAuthority的抽象方法,所以我们这里就简单的用内部类创建了一个类,使用流给他转换成权限对象数组
这里的设计模式很有意思
@Data
@AllArgsConstructor
public class AdminUserDetails implements UserDetails {
// id
private Long id;
// 用户名
private String username;
// 密码
private String password;
// 是否封禁
private Boolean nonBan;
// 是否启用
private Boolean enabled;
// 权限列表
private List<EnSysAdminAuth> userAuths;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.userAuths.stream().map(auth -> (GrantedAuthority) auth::getAuthTag).collect(Collectors.toList());
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return this.nonBan;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
配置Security核心配置
注意!拦截规则配置不能包含项目地址,
也就是配置文件中配置的server.servlet.context-path: /enddmin
如果你的访问地址是/aa/bb/cc
你配置的项目地址是/aa/bb
这里写规则就只能写/cc
就像:
.antMatchers("/cc").permitAll()
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_WHITELIST = {
// -- swagger ui
"/swagger-resources/**",
"/swagger-ui.html",
"/v2/api-docs",
"/webjars/**",
// -- h2 database console
"/h2-console/**",
"/test/**",
// -- 静态资源
"/**"
};
@Autowired
private UserDetailLoad userDetailLoad;
@Autowired
private AdminSysAuthDao adminSysAuthDao;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private TranslationTools translationTools;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 配置身份验证管理器,配置一下用户来源,同时配置密码加解密工具
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailLoad).passwordEncoder(passwordEncoder);
}
// 配置security相关配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().cors().and()
// 关闭session,注意:关闭了这个以后会话将不能维持登录状态,需要自己在每次请求的时候建立用户详情对象到线程域中;
// 一般前后端分离的时候选择这个,不分离可以用会话管理,当然你不分离想用session也可以
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 禁用session
//http.addFilterAt(jwtLoginFilter, UsernamePasswordAuthenticationFilter.class);// 添加我们的过滤器,覆盖原先的过滤器
AdminCheckTokenFilter adminCheckTokenFilter =new AdminCheckTokenFilter(translationTools,adminJwtUtils,userDetailLoad);
http.addFilterBefore(adminCheckTokenFilter, UsernamePasswordAuthenticationFilter.class); // 在鉴权前放入我们的token鉴权,如果token通过就当作他通过验证了
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry httpAuth = http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/sys-user/login").permitAll()
.antMatchers(HttpMethod.PUT, "/sys-user/logout").permitAll()
.antMatchers(AUTH_WHITELIST).permitAll();
// 查询权限配置权限对应的访问规则
List<PzhSysAuth> allAuth = adminSysAuthDao.findAll();
allAuth.forEach(auth -> httpAuth.antMatchers(auth.getAuthUri()).hasAnyAuthority(auth.getAuthTag(), "all"));
// 其他请求,完全认证以后才能登录
httpAuth.anyRequest()
.authenticated();
// 异常处理
http.exceptionHandling()
// 身份认证未通过,比如:在访问需要认证权限的接口时,线程域中没有用户安全信息;
.authenticationEntryPoint((req, rsp, e) -> {
ResponseUtils.err(403,rsp,new Result<>(false, CodeEnum.PLEASE_LOG_IN_FIRST.getCode(),translationTools.get(CodeEnum.PLEASE_LOG_IN_FIRST)));
})
// 权限不足,请求被拒绝
.accessDeniedHandler((req, rsp, e) -> {
ResponseUtils.err(403,rsp,new Result<>(false, CodeEnum.NEED_PERMISSION_TO_DO.getCode(),translationTools.get(CodeEnum.NEED_PERMISSION_TO_DO)));
});
}
// 当出现无法注入bean【AuthenticationManager】时添加
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 处理跨域问题
*
* @return
*/
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
configuration.setAllowedMethods(Arrays.asList("OPTIONS", "GET", "POST", "PUT", "DELETE"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
一个拦截规则配置样例
注意!拦截规则配置不能包含项目地址,
也就是配置文件中配置的server.servlet.context-path: /enddmin
如果你的访问地址是/aa/bb/cc
你配置的项目地址是/aa/bb
这里写规则就只能写/cc
就像:
.antMatchers("/cc").permitAll()
放行/user目录:/user
放行/user目录下的同级文件:/user/*
放行/user目录之后的所有层级文件,也就是更深层次的也放行/user/**
// 访问配置
http.authorizeRequests()
// 路径不用管资源前缀
.antMatchers(HttpMethod.POST,"/adminUser/login").permitAll()
.antMatchers(HttpMethod.PUT,"/adminUser/logout").permitAll()
// 表示adminUser下的一级目录可以访问
.antMatchers(HttpMethod.GET,"/adminUser/*").permitAll()
// 表示role目录下的所有资源,包括更深层可以访问
.antMatchers(HttpMethod.GET,"/role/**").permitAll()
// 仅表示adminUser这个路径需要roots权限
.antMatchers(HttpMethod.GET,"/adminUser").hasAnyAuthority("roots")
// 所有请求需要认证
.anyRequest().authenticated();
添加Jwt工具
@Slf4j
@Component
public class AdminJwtUtils {
@Autowired
private AdminJwtConfig jwtConfig;
@Autowired
private HttpServletRequest request;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* token校验
*
* @param token 需要检查的token
* @return
*/
public PzhSysUser checkToken(String token) {
try {
Claims body = Jwts.parser()
.setSigningKey(jwtConfig.getSecretKey())
.parseClaimsJws(token)
.getBody();
// token版本校验
String tokenVersion = redisTemplate.opsForValue().get(RedisKeyEnum.TOKEN_VERSION.getKey() + body.get("id").toString());
if (StringUtils.isBlank(tokenVersion) || !StringUtils.equals(body.get("version").toString(), tokenVersion))
throw new TokenInvalidException(401, CodeEnum.INVALID_TOKEN.getCode(), "token:" + token + "登录时token版本失效");
PzhSysUser sysUser = new PzhSysUser();
sysUser.setId(Long.valueOf(body.get("id").toString()));
return sysUser;
} catch (JwtException | TokenInvalidException e) {
throw new TokenInvalidException(401, CodeEnum.INVALID_TOKEN.getCode(), "token无效:" + token);
} catch (Exception e) {
throw new TokenInvalidException(401, CodeEnum.INVALID_TOKEN.getCode(), "token校验异常:" + token);
}
}
/**
* 刷新token,当token失效时调用的token
*
* @param refreshToken 刷新用token
* @return
*/
public PzhTokens refreshToken(String refreshToken) {
try {
Claims body = Jwts.parser()
.setSigningKey(jwtConfig.getRefreshTokenSecretKey())
.parseClaimsJws(refreshToken)
.getBody();
// token版本校验
String tokenVersion = redisTemplate.opsForValue().get(RedisKeyEnum.TOKEN_VERSION.getKey() + body.get("id").toString());
if (StringUtils.isBlank(tokenVersion) || !StringUtils.equals(body.get("version").toString(), tokenVersion))
throw new TokenInvalidException(401, CodeEnum.INVALID_TOKEN.getCode(), "token:" + refreshToken + "刷新token时刷新token版本失效");
if (ObjectUtil.isEmpty(body))
throw new TokenInvalidException(401, CodeEnum.INVALID_TOKEN.getCode(), "token:" + refreshToken + "刷新token时刷新token无效");
return getTokens(Long.valueOf(body.get("id").toString()));
} catch (JwtException | TokenInvalidException e) {
throw new TokenInvalidException(401, CodeEnum.INVALID_TOKEN.getCode(), "token无效:" + refreshToken);
} catch (Exception e) {
throw new TokenInvalidException(401, CodeEnum.INVALID_TOKEN.getCode(), "token校验异常:" + refreshToken);
}
}
/**
* 获取token对象
*
* @return
*/
public PzhTokens getTokens(Long userId) {
String tokenVersion = UUID.randomUUID().toString();
PzhTokens tokens = PzhTokens.builder()
.token(getToken(userId, tokenVersion, jwtConfig.getSecretKey(), jwtConfig.getTokenExpiration()))
.expTime(System.currentTimeMillis() + jwtConfig.getTokenExpiration() * 60 * 1000)
.refreshToken(getToken(userId, tokenVersion, jwtConfig.getRefreshTokenSecretKey(), jwtConfig.getRefreshTokenExpiration()))
.refreshExpTime(System.currentTimeMillis() + jwtConfig.getRefreshTokenExpiration() * 60 * 1000)
.build();
// 返回前写入版本信息,同时使之前的版本信息失效
redisTemplate.opsForValue().set(
RedisKeyEnum.TOKEN_VERSION.getKey() + userId, tokenVersion,
Duration.ofMinutes(jwtConfig.getRefreshTokenExpiration()));
return tokens;
}
/**
* 获取token,内部调用使用,根据用户id以及盐生成token,同时绑定版本号
*
* @return
*/
private String getToken(Long userId, String tokenVersion, String secret, Integer expiration) {
Map<String, Object> claims = new HashMap<>();
claims.put("id", userId);
claims.put("version", tokenVersion);
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, secret)
.setExpiration(new DateTime().plusMinutes(expiration).toDate())
.compact();
// 生成token以后需要更新这个token对应的版本号,这里没有要求就不设置了
}
/**
* 检查当前用户是否为登录状态,主要用于敏感操作前的权限鉴定
*
* @return
*/
public PzhSysUser checkCurrentRequestAuth() {
String token = request.getHeader("Authorization");
try {
if (StringUtils.isNotBlank(token))
return checkToken(token);
} catch (JwtException | TokenInvalidException e) {
// 不处理。只为捕获检查token抛出的异常。这里直接判定为未登录即可
}
return null;
}
/**
* 删除用户的token版本。使登录失效
*
* @param userId
*/
public Boolean removeTokenVersion(Long userId) {
return redisTemplate.delete(RedisKeyEnum.TOKEN_VERSION.getKey() + userId);
}
}
jwt过滤器
过滤器可以使用OncePerRequestFilter
,GenericFilterBean
等都行,
这里的目的主要是为了把token里的用户加载到线程域
,如果token不符合规范或者错误等,直接放行就可以了,如果我们没有把他放进线程域,后面的过滤器
不会放过他的,除非是被排除的公共uri;
一个过滤器样例
public class JwtFilter implements Filter {
private final AdminJwtUtils jwtUtils;
public JwtFilter(AdminJwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("token");
if (StringUtils.isNotBlank(token)){
UserDetails user = jwtUtils.parseToken(token);
if (user!=null){
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
user,"", user.getAuthorities()
));
}
}
chain.doFilter(request, response);
}
}
优化以后的过滤器写法
原来直接放行是没什么问题,可是如果遇到token过期和token无效那么返沪的异常确是因为没有写入用户带来的403.虽然没什么问题,但是还是不太何是,所以加了判断+
@Slf4j
@Component
public class AdminCheckTokenFilter extends OncePerRequestFilter {
public AdminCheckTokenFilter(TranslationTools translationTools, AdminJwtUtils adminJwtUtils, UserDetailLoad userDetailLoad) {
this.translationTools = translationTools;
this.adminJwtUtils = adminJwtUtils;
this.userDetailLoad = userDetailLoad;
}
private final TranslationTools translationTools;
private final AdminJwtUtils adminJwtUtils;
private final UserDetailLoad userDetailLoad;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 如果在放行列表里,则放行;否则进行token判断
if (Arrays.asList(WebSecurityConfig.AUTH_WHITELIST).contains(request.getServletPath())) {
filterChain.doFilter(request, response);
return;
}
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token))
ResponseUtils.err(401, response, new Result<>(true, CodeEnum.TOKEN_EXIST.getCode(),translationTools.get(CodeEnum.TOKEN_EXIST)));
PzhSysUser sysUser;
try {
sysUser = adminJwtUtils.checkToken(token);
if (ObjectUtil.isEmpty(sysUser))
throw new TokenInvalidException(401,CodeEnum.INVALID_TOKEN.getCode(),"token无效");
} catch (Exception e) {
logger.error(e.getMessage());
ResponseUtils.err(401, response, new Result<>(false, CodeEnum.INVALID_TOKEN.getCode(),translationTools.get(CodeEnum.INVALID_TOKEN)));
return;
}
// 至此认证通过
UserDetails userDetails = null;
try {
// 加载用户详情
userDetails = userDetailLoad.loadUserByUsername(sysUser.getEmail());
} catch (UsernameNotFoundException e) {
logger.error(e.getMessage());
ResponseUtils.err(403,response,new Result<>(false,CodeEnum.USER_NOT_EXISTS.getCode(),translationTools.get(CodeEnum.USER_NOT_EXISTS)));
return;
}
// 将用户详情封装为权限认证信息
Authentication authenticate = new UsernamePasswordAuthenticationToken(userDetails,"", userDetails.getAuthorities());
// 将用户信息放入线程域对象
SecurityContextHolder.getContext().setAuthentication(authenticate);
filterChain.doFilter(request,response);
SecurityContextHolder.clearContext();
}
}
过滤器添加方法一
/**
* 继承了类SecurityConfigurerAdapter
*/
public class JwtConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final TokenService tokenService;
public JwtConfigurer(TokenService tokenService) {
this.tokenService = tokenService;
}
/**
* 重写了SecurityConfigurerAdapter中的一个configure方法
* 添加了一个过滤器
* 动作、加载了一个请求过滤器
* @param http
*/
@Override
public void configure(HttpSecurity http) {
// 获得过滤器,同时传入构造所需的jjwt工具类
JwtTokenFilter customFilter = new JwtTokenFilter(tokenService);
// 使用添加到之前方法,将过滤器在usernamepassword过滤器执行之前添加【usernamepswod这个过滤器是校验用户的,而我们将在filter过滤器中手动完成用户的添加】
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
过滤器添加方法二
关于内存泄漏
使用完成后需要对上下文对象进行清理
// 将用户信息放入线程域对象
SecurityContextHolder.getContext().setAuthentication(authenticate);
UserThreadLocal.set(carrier);
filterChain.doFilter(request, response);
UserThreadLocal.remove();
SecurityContextHolder.clearContext();
后续遇到的一些问题
关于自己在过滤器里
对公开资源进行放行遇到的问题,
导致正则不能正确匹配,因为这里只是进行了简单的字符串对比
,比如规则是/user/**
而这里取uri却是/user/login
,字符串比对不通过所以就拒绝了
通过修改到如下内容,不再对公开内容进行放行,交由security
处理,
不再响应token无效等异常,统一由security
处理后返回被拒绝
或者身份认证不通过
如果实在要根据token状态在这里进行返回的话,那就自己实现一个规则和security
一样的,允许使用正则匹配的放啊进行校验;
public class AdminCheckTokenFilter extends OncePerRequestFilter {
public AdminCheckTokenFilter(TranslationTools translationTools, AdminJwtUtils adminJwtUtils, UserDetailLoad userDetailLoad) {
this.translationTools = translationTools;
this.adminJwtUtils = adminJwtUtils;
this.userDetailLoad = userDetailLoad;
}
private final TranslationTools translationTools;
private final AdminJwtUtils adminJwtUtils;
private final UserDetailLoad userDetailLoad;
@SuppressWarnings("DuplicatedCode")
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String token = request.getHeader("Authorization");
if (StringUtils.isNotBlank(token)){
PzhSysUser sysUser;
// 出现任何错误就直接交给下一个过滤器,就不向线程里放用户认证信息了,稍后就会被过滤器阻拦
try {
sysUser = adminJwtUtils.checkToken(token);
if (sysUser==null){
filterChain.doFilter(request,response);
return;
}
} catch (Exception e) {
//
filterChain.doFilter(request,response);
return;
}
// 至此认证通过
UserDetails userDetails;
try {
// 加载用户详情
userDetails = userDetailLoad.loadUserByUsername(sysUser.getEmail());
} catch (UsernameNotFoundException e) {
logger.error(e.getMessage());
ResponseUtils.err(403,response,new Result<>(false,CodeEnum.USER_NOT_EXISTS.getCode(),translationTools.get(CodeEnum.USER_NOT_EXISTS)));
return;
}
// 将用户详情封装为权限认证信息
Authentication authenticate = new UsernamePasswordAuthenticationToken(userDetails,"", userDetails.getAuthorities());
// 将用户信息放入线程域对象
SecurityContextHolder.getContext().setAuthentication(authenticate);
}
filterChain.doFilter(request,response);
log.info("token过滤器执行完毕");
} catch (Exception e) {
throw e;
}finally {
SecurityContextHolder.clearContext();
}
}
}
后续的补充修改,修改为除了允许的以及配置权限的全部拒绝
之前的设置是
http.httpBasic().disable()
.csrf().disable().cors().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 禁用session
// 配置放行白名单同时获取到拦截注册表
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry httpAuth = http.authorizeRequests()
.antMatchers(AUTH_WHITELIST).permitAll();
// 查询权限配置权限对应的访问规则
List<PzhSysAuth> allAuth = adminSysAuthDao.findAll();
allAuth.forEach(auth -> httpAuth
.antMatchers(auth.getAuthUri()).hasAnyAuthority(auth.getAuthTag(), "all"));
// 其他请求,完全认证以后才能登录
httpAuth.anyRequest()
.authenticated();
1.放行白名单里的请求
2.给部分资源设置权限级别
3.其他资源全部需要认证(登录)以后才能访问
====
现在遇到的情况是,除了配置权限的资源,以及设置了白名单的资源,其他的请求只要用户登录了都可以访问;
现在需要修改为,除了放行的以及设置了权限的,其他全部不在我们管理范围内的资源需要全部拒绝。
我们做出如下修改
注意这个denyAll仅适合通过url配置权限的方式
,如果使用注解的方式配置的权限会导致配置无效,除了上面配置了放行和规则的路径,其他的全都会被拒绝,
这里的意思就是说.除了上面我们配置过的…其他请求全部拒绝…
httpAuth.anyRequest()
// .authenticated(); 其他请求都需要认证以后才能访问
.denyAll(); // 修改为其他请求全部拒绝,没有在上面声明放行的路由以妹有设置权限的路由全部拒绝
2022-10-19新项目配置文件
用于方便以后直接抄
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 配置用户加载器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new UserDetailLoadService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 初始化配置
http.httpBasic().disable()
// 关闭csrf否则默认拒绝post请求
.csrf().disable().cors().and()
// 关闭session,注意:关闭了这个以后会话将不能维持登录状态,需要自己在每次请求的时候建立用户详情对象到线程域中;
// 一般前后端分离用jwt的时候选择关闭这个,然后解析以后赋予身份信息,在请求结束的时候销毁
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 访问配置
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/adminUser/login").permitAll()
.antMatchers(HttpMethod.PUT, "/adminUser/logout").hasAnyAuthority("root")
// 所有请求需要认证
.anyRequest().authenticated();
// 处理异常登录
http.exceptionHandling()
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
ResponseUtils.writeAndFlush(response, new Result<>(false, 403, "访问被拒绝", accessDeniedException.getMessage()));
})
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
ResponseUtils.writeAndFlush(response, new Result<>(false, 401, "未完成身份认证", authException.getMessage()));
});
}
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 处理跨域问题
*
* @return
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
configuration.setAllowedMethods(Arrays.asList("OPTIONS", "GET", "POST", "PUT", "DELETE"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
使用Security自带的认证器进行密码校验
当然你自己写密码进行校验也可以:
Session模式:
密码校验成功后把UserDetail设置进线程域即可
Token模式
密码校验成功后把token返回,通过filter在每次请求时验证token,成功后依然是把UserDetail设置进线程域
自己密码校验:
用自己的方式完成校验,然后按照上面两条操作即可
使用Securiry提供的认证管理器
1.我们需要实现一个接口UserDetailsService
,完成方法以后,再实现一个接口PassWordEncod
实现加解密方法(可以使用Security提供的几个实现比如BCryptPasswordEncoder)
2.然后我们创建一个UsernamePasswordAuthenticationToken
构造器参数是账号和密码,这相当于用账号密码填写一个表单
3.在SecurityConfig中写入用户UserDetailServic
以及PasswordEncode
// 配置用户加载器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置用户加载器和加解密器这样就不用自己写判断逻辑了
auth.userDetailsService(userDetailLoadService)
.passwordEncoder(passwordEncoder);
}
4.调用AuthenticationManager
的authenticate
认证方法(此时会触发我们上面定义的UserLoad和PasswrodEncode),完成认证以后如果不通过就会抛出异常,通过表示认证完成,返回值里可以直接提取UserDetail进行写入线程域
如果提示找不到AuthenticationManager
我们需要在SecurtiyConfig中重写并注入这个Bean,上面的配置文件也有提到
实现这个的配置文件中进行配置即可,
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
下面是伪代码部分
完成认证
@Override
public LoginSuccessView login(LoginParam loginParam) {
// 获取认证令牌
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(
loginParam.getUsername(),
loginParam.getPassword()
);
// 调用身份认证管理器进行认证(记不记得这个,这是我们在SecurityConfig里注入的那个认证Bean)
// 调用这步完成认证时就会触发我们配置的UserDetailLoader以及我们配置的PasswordEncode两个类
try {
Authentication authenticate = authenticationManager.authenticate(upToken);
// 认证成功,设置身份信息(这里是基于session),使用token需要自己在filter创建写入
// 日后从线程域get出来的信息就是我们UserDetailLoder里返回的那个
SecurityContextHolder.getContext().setAuthentication(authenticate);
} catch (AuthenticationException e) {
// 抛出异常则是认证失败,通常我们在这里对错误次数进行计数并返回认证失败
throw new RuntimeException("认证失败:"+e.getMessage());
}
return new LoginSuccessView("tk","xxx","xxx",new ArrayList<>(),"xxx");
}
用户加载
@Configuration
public class UserDetailLoadService implements UserDetailsService {
private final AdUserService adUserService;
private final AdAuthService adAuthService;
public UserDetailLoadService(AdUserService adUserService, AdAuthService adAuthService) {
this.adUserService = adUserService;
this.adAuthService = adAuthService;
}
// 日后从线程域get出来的信息就是我们UserDetailLoder里返回的那个
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<AdUser> wrapper = Wrappers.lambdaQuery();
wrapper.eq(AdUser::getUsername,username);
AdUser user = adUserService.getOne(wrapper);
List<AdAuth> auth = adAuthService.queryAuthByUserId(user.getId());
return AdminUserForSecurity.builder()
.enable(user.getHasEnable())
.lock(user.getHasBan())
.password(user.getPassword())
.userAuths(auth.stream().map(AdAuth::getAuthTage).filter(StringUtils::isNoneBlank).distinct().collect(Collectors.toList()))
.build();
}
}