关于权限,一直都存在争议,从最早的切面控制到现在的权限框架,不得不说是一种解放程序员双手的进步。
本文主要记录SpringSecurity+Jwt的配置与使用,通过不同的路径来控制不同的权限
介绍
SpringSecurity是一个能够为基于Spring的应用系统提供声明式的安全访问控制的安全框架,底层实现了一条一条过滤链路,用户请求进来,先判断用户的请求权限,再继续往下走,如果无权限的则抛出异常。
为系统提供了声明式的安全访问控制功能,极大的减少了为系统安全控制编写的重复代码。
SpringSecurity对Web安全的支持大量的依赖于Servlet过滤器。这些过滤器拦截进入请求,并在应用程序处理之前进行某些安全处理。
1.环境依赖
基于spring-boot-starter-parent 2.6.8
,依赖引入
<!--Spring boot 安全框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
2.验证规则代码
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
ApplicationContext applicationContext;
// @Bean
// GrantedAuthorityDefaults grantedAuthorityDefaults() {
// // 去除 hasRole ROLE_ 前缀 实测不生效 暂时不用
// return new GrantedAuthorityDefaults("");
// }
@Bean
public PasswordEncoder passwordEncoder() {
// 密码加密方式
return new BCryptPasswordEncoder();
}
/**
* 一些通用的api允许访问
*
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
this.getDefaultConfigure(http);
}
public void getDefaultConfigure(HttpSecurity http) throws Exception {
// 获取匿名标记
Map<String, Set<String>> anonymousUrls = this.getAnonymousUrl();
http.csrf().disable()
// 授权异常
.exceptionHandling()
//403处理类 无法访问
.authenticationEntryPoint(getAuthenticationEntryPoint())
//401处理类 身份信息验证错误
.accessDeniedHandler(getAccessDeniedHandler())
.and()
.authorizeRequests()
// 允许所有的白名单路径 swagger actuator等等 String[]
.antMatchers(SecurityConstant.PASS_PATH_GROUP).permitAll()
// 允许所有的 OPTIONS
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 自定义注解匿名访问所有url放行:允许匿名和带Token访问,细腻化到每个 Request 类型
// GET
.antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
// POST
.antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
// PUT
.antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
// PATCH
.antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll()
// DELETE
.antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll()
// 所有类型的接口都放行
.antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll()
//默认全部放行
// .antMatchers("/**").permitAll()
//前后台jwt中默认的权限 /服务名/api -> app接口 /服务名/admin -> 管理后台接口
//hasRole 需要在权限面前加上ROLE_ hasAuthority/则不需要
.antMatchers("/api/**").hasAuthority(SecurityConstant.AUTH_USER)
.antMatchers("/admin/**").hasAuthority(SecurityConstant.AUTH_MANAGER)
// 所有请求都需要认证
.anyRequest().authenticated()
//添加过滤器
.and().addFilter(this.getDefaultFilter());
}
public AccessDeniedHandler getAccessDeniedHandler() {
return (request, response, e) -> {
log.error("403 Forbidden,URI:{}", request.getRequestURI());
//当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
};
}
public AuthenticationEntryPoint getAuthenticationEntryPoint() {
return (request, response, e) -> {
log.error("401 无凭据,{},URI:{}", request.getMethod(), request.getRequestURI());
// 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e == null ? "Unauthorized" : e.getMessage());
};
}
//默认的jwt校验器 可由子类覆盖实现
public BasicAuthenticationFilter getDefaultFilter() throws Exception {
return new JwtAuthorizationFilter(authenticationManager());
}
public Map<String, Set<String>> getAnonymousUrl() {
// 搜寻匿名标记 url: @AnonymousAccess
RequestMappingHandlerMapping requestMappingHandlerMapping =
(RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
// 获取匿名标记
Map<String, Set<String>> anonymousUrls = new HashMap<>(6);
Set<String> get = new HashSet<>();
Set<String> post = new HashSet<>();
Set<String> put = new HashSet<>();
Set<String> patch = new HashSet<>();
Set<String> delete = new HashSet<>();
Set<String> all = new HashSet<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
HandlerMethod handlerMethod = infoEntry.getValue();
AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
if (null != anonymousAccess) {
List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
switch (Objects.requireNonNull(request)) {
case GET:
get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case POST:
post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case PUT:
put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case PATCH:
patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case DELETE:
delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
default:
all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
}
}
}
anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);
return anonymousUrls;
}
}
3.权限过滤器
BasicAuthenticationFilter
的继承类
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String token = JwtUtils.getToken(req);
if (token == null) {
//可能会存在前端session缓存问题
SecurityContextHolder.getContext().setAuthentication(null);
chain.doFilter(req, res);
return;
}
try {
//验证token 并取出对应信息 在这里检验权限
Authentication authentication = JwtUtils.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
//子类自定义操作 例如 存储,redis过期/续期
this.handler(token, authentication);
chain.doFilter(req, res);
} catch (Exception e) {
GlobalExceptionEnum ex = GlobalExceptionEnum.TOKEN_IS_ERROR;
logger.error(e.getMessage());
res.setContentType("application/json; charset=utf-8");
res.setStatus(HttpStatus.UNAUTHORIZED.value());
res.getWriter().write("{\"code\": " + HttpStatus.UNAUTHORIZED.value() + ", \"message\": \"" + ex.getText() + "\"}");
res.getWriter().close();
}
}
//getAuthentication
/**
* 依据Token 获取鉴权信息
* @param token /
* @return /
*/
public static Authentication getAuthentication(String token) {
Claims claims = parseToken(token);
List grantedAuthoritys = (List) claims.get(SecurityConstant.CLAIMS);
List<GrantedAuthority> authorities = new ArrayList<>();
boolean isManage = false;
for (Object item : grantedAuthoritys) {
Map<String, Object> map = (Map<String, Object>) item;
authorities.add(new SimpleGrantedAuthority(map.get("authority").toString()));
if (SecurityConstant.AUTH_MANAGER.equals(map.get("authority").toString())) {
isManage = true;
}
}
if (isManage) {
User principal = new User(claims.getSubject(), "******", authorities);
return new UsernamePasswordAuthenticationToken(principal, null, authorities);
}
//authorities 生成jwt设置
return new UsernamePasswordAuthenticationToken(claims.getSubject(), null, authorities);
}
4.token的创建和解析
private static JwtParser jwtParser;
private static JwtBuilder jwtBuilder;
private static Key key;
static {
/**
* 512秘钥生成方法 jwt0.9.1以后
* Key key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
* String secretString = Encoders.BASE64.encode(key.getEncoded());
*/
key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SecurityConstant.SECRET));
jwtParser = Jwts.parserBuilder()
.setSigningKey(key)
.build();
jwtBuilder = Jwts.builder()
.signWith(key, SignatureAlgorithm.HS512);
}
/**
* 不设过期时间 需要过期处理的可用到redis
* @param username
* @param authorities 权限列表
* @return
*/
public static String createToken(String username, List<GrantedAuthority> authorities) {
String token = jwtBuilder
.setSubject(username)
.claim(SecurityConstant.CLAIMS, authorities)
.compact();
return token;
}
//token解析
public static Claims parseToken(String token) {
return jwtParser.parseClaimsJws(token).getBody();
}
5.生成jwt并授权
//授权组
List<GrantedAuthority> authorities = new ArrayList<>();
// 昵称#id#uuid
authorities.add(new SimpleGrantedAuthority(userInfo.getNickname()
+ SecurityConstant.AUTH_SEPARATOR + userInfo.getId()
+ SecurityConstant.AUTH_SEPARATOR + userInfo.getUuid()));
//权限字段
authorities.add(new SimpleGrantedAuthority(SecurityConstant.AUTH_USER));
//添加时间戳是保证每次生成token不一致 用于单点登录
authorities.add(new SimpleGrantedAuthority(new Date().getTime() + ""));
// 生成令牌
return JwtUtils.createToken(userInfo.getNickname(), authorities);
6.获取uid和检验权限
/**
* 获取当前登录用户的UID
*
* @return
*/
public static Long getLoginUid() {
List<GrantedAuthority> grantList = (List<GrantedAuthority>) SecurityContextHolder.getContext().getAuthentication().getAuthorities();
if (grantList.size() > 1) {
String[] authoritys = grantList.get(0).getAuthority().split(SecurityConstant.AUTH_SEPARATOR);
if (authoritys.length >= 2) {
return Long.valueOf(authoritys[1]);
}
}
return null;
}
/**
* 是否包含该权限
*
* @param roleNames
* @return
*/
public static boolean containsRole(String... roleNames) {
List<GrantedAuthority> grantList = (List<GrantedAuthority>) SecurityContextHolder.getContext().getAuthentication().getAuthorities();
for (GrantedAuthority authority : grantList) {
for (String roleName : roleNames) {
if (roleName.equals(authority.getAuthority())) return true;
}
}
return false;
}
7.基于框架授权模式
即登录账号密码直接传入框架,然后实现框架内的接口给予授权
//用账户密码登录的
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(authUser.getUsername(), password);
//最终回调到UserDetailsService#loadUserByUsername 方法 该方法需要自己实现 验证用户信息和返回授权List<GrantedAuthority>
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
//能得到是user信息 授权信息 等 看自己实现
final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal();
在UserDetailsService#loadUserByUsername
的实现中进行授权,最终返回一个UserDetail
接口实现对象
以上就是本章的全部内容了。