1、RESTfull风格的鉴权服务(路线相同的情况下根据请求方式鉴别访问权限)
2、包含用户、角色、权限
3、使用JWT最为token认证方式
传统的单体应用体系下,应用是一个整体,一般针对所有的请求都会进行权限校验。请求一般会通过一个权限的拦截器进行权限的校验,在登录时将用户信息缓存到 session 中,后续访问则从缓存中获取用户信息
但在微服务架构下,一个应用会被拆分成若干个微应用,每个微应用都需要对访问进行鉴权,每个微应用都需要明确当前访问用户以及其权限。尤其当访问来源不只是浏览器,还包括其他服务的调用时,单体应用架构下的鉴权方式就不是特别合适了。因此在设计架构中,要考虑外部应用接入的场景、用户与服务的鉴权、服务与服务的鉴权等多种鉴权场景。
目前主流的方案由四种
一次登入,多地使用。这种方案意味着每个面向用户的服务都必须与认证服务交互,进而产生大量琐碎的网络流量和重复的工作,当动辄数十个微应用时,这种方案的弊端会更加明显。
借助reids或其他共享存储中,将用户认证的信息存储在其中,通常使用用户会话作为 key 来实现的简单分布式哈希映射。当用户访问微服务时,用户数据可以从共享存储中获取。在某些场景下,这种方案很不错,用户登录状态是不透明的。同时也是一个高可用且可扩展的解决方案。这种方案的缺点在于共享存储需要一定保护机制,因此需要通过安全链接来访问,这时解决方案的实现就通常具有相当高的复杂性了。
令牌在客户端生成,由身份验证服务进行签名,并且必须包含足够的信息,以便可以在所有微服务中建立用户身份。令牌会附加到每个请求上,为微服务提供用户身份验证,这种解决方案的安全性相对较好,但身份验证注销是一个大问题,缓解这种情况的方法可以使用短期令牌和频繁检查认证服务等。对于客户端令牌的编码方案,Borsos 更喜欢使用 JSON Web Tokens(JWT),它足够简单且库支持程度也比较好。
这个方案意味着所有请求都通过网关,从而有效地隐藏了微服务。 在请求时,网关将原始用户令牌转换为内部会话 ID 令牌。在这种情况下,注销就不是问题,因为网关可以在注销时撤销用户的令牌。
本文就采用方案4,实现微服务体系中用户鉴权及认证服务。
Token的实现方案业界有多套成熟的方案,这其中最主流的是JWT 和 Oauth2.0 两种方式。
下面就基于JWT的方式具体实现。
AuthenticationManager, 用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManager的authenticate()方法来实现。当然事情肯定不是它来做,具体校验动作会由AuthenticationManager将请求转发给具体的实现类来做。根据实现反馈的结果再调用具体的Handler来给用户以反馈。
AuthenticationProvider, 认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风,主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。
前面讲了AuthenticationManager只是一个代理接口,真正的认证就是由AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider,每个provider通过实现一个support方法来表示自己支持那种Token的认证。AuthenticationManager默认的实现类是ProviderManager。
UserDetailService, 用户认证通过Provider来做,所以Provider需要拿到系统已经保存的认证信息,获取用户信息的接口spring-security抽象成UserDetailService。虽然叫Service,但是我更愿意把它认为是我们系统里经常有的UserDao。
AuthenticationToken, 所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如最容易理解的UsernamePasswordAuthenticationToken。
SecurityContext,当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过SecurityUtils.getSubject()到达同样的目的。
完整pom文件(项目结构为多模块)
springcloud
com.lhm
1.0
4.0.0
security
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt
0.9.0
com.baomidou
mybatis-plus-boot-starter
3.1.0
p6spy
p6spy
3.8.1
mysql
mysql-connector-java
runtime
com.alibaba
druid-spring-boot-starter
1.1.9
org.apache.commons
commons-pool2
org.springframework.boot
spring-boot-starter-data-redis
com.alibaba
fastjson
1.2.38
org.apache.commons
commons-lang3
3.4
org.projectlombok
lombok
provided
在登入方面,本次使用了security默认提供的表单登陆方式,因此直接从实现UserDetailsService开始
package com.lhm.springcloud.security.service.impl;
import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.exception.CommonException;
import com.lhm.springcloud.security.pojo.AuthUserDetails;
import com.lhm.springcloud.security.pojo.AuthUserPoJo;
import com.lhm.springcloud.security.service.IUsersService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
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.Component;
/**
* @ClassName UserDetailsServiceImpl
* @Description 实现security提供的 用户信息获取接口 并按照业务增加redis 登陆限制
* @Author liuheming
* @Date 2019/5/6 10:26
* @Version 1.0
**/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
//登入重试时间
@Value("${security.loginAfterTime}")
private Integer loginAfterTime;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private IUsersService iUsersService;
/**
* @Author liuheming
* @Description 实现用户信息查询方法 让DaoAuthenticationProvider 获取到数据库获中用户数据
* @Date 11:21 2019/5/6
* @Param [username]
* @return org.springframework.security.core.userdetails.UserDetails
**/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String flagKey = "loginFailFlag:"+username;
String value = redisTemplate.opsForValue().get(flagKey);
if(StringUtils.isNotBlank(value)){
//超过限制次数
throw new UsernameNotFoundException("登录错误次数超过限制,请"+loginAfterTime+"分钟后再试");
}
//查询用户信息
AuthUserPoJo authUserPoJo=iUsersService.findAuthUserByUsername(username);
if(null==authUserPoJo){
throw new UsernameNotFoundException("当前用户不存在");
}
if(authUserPoJo.getRoleInfos()==null || authUserPoJo.getRoleInfos().isEmpty()){
throw new UsernameNotFoundException("当前用户无角色");
}
return new AuthUserDetails(authUserPoJo);
}
}
UserDetailsServiceImpl 最后返回一个拼装好的security用户对象,但为了实现自定义角色与权限管理需要对UserDetails进行重写。
package com.lhm.springcloud.security.pojo;
import com.lhm.springcloud.security.constant.UserConstant;
import com.lhm.springcloud.security.entity.PermissionInfo;
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;
/**
* @author Exrickx
*/
public class AuthUserDetails extends AuthUserPoJo implements UserDetails {
private static final long serialVersionUID = 1L;
public AuthUserDetails(AuthUserPoJo user) {
if (user != null) {
this.setUserName(user.getUserName());
this.setPassWord(user.getPassWord());
this.setStatus(user.getStatus());
this.setRoleInfos(user.getRoleInfos());
this.setPermissionInfos(user.getPermissionInfos());
}
}
//将角色权限 放入GrantedAuthorit的自定义实现类MyGrantedAuthority中 为权限判定提供数据
@Override
public Collection extends GrantedAuthority> getAuthorities() {
List authorityList = new ArrayList();
List permissions = this.getPermissionInfos();
if (permissions != null) {
for (PermissionInfo permission : permissions) {
GrantedAuthority grantedAuthority = new MyGrantedAuthority(permission.getPath(), permission.getMethod());
authorityList.add(grantedAuthority);
}
}
return authorityList;
}
@Override
public String getPassword() {
return super.getPassWord();
}
@Override
public String getUsername() {
return super.getUserName();
}
/**
* 账户是否过期
*
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 是否禁用
*
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 密码是否过期
*
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否启用
*
* @return
*/
@Override
public boolean isEnabled() {
return UserConstant.USER_STATUS_NORMAL.equals(this.getStatus()) ? true : false;
}
}
然后DaoProvider会对比校验并执行相应的结果处理器
登入成功处理器
package com.lhm.springcloud.security.handler;
import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.pojo.AuthUserDetails;
import com.lhm.springcloud.security.utils.ResUtil;
import com.lhm.springcloud.security.utils.ResponseUtil;
import com.lhm.springcloud.security.utils.TokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
/**
* @ClassName LoginSuccessHandlerFilter
* @Description 登陆认证成功处理过滤器
* @Author liuheming
* @Date 2019/5/6 16:27
* @Version 1.0
**/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private TokenUtil tokenUtil;
/**
* @Author liuheming
* @Description 用户认证成功后 生成token并返回
* @Date 8:50 2019/5/7
* @Param [request, response, authentication]
* @return void
**/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
AuthUserDetails authUserDetails=(AuthUserDetails)authentication.getPrincipal();//从内存中获取当前认证用户信息
//创建token
String accessToken = tokenUtil.createAccessJwtToken(authUserDetails);
String refreshToken = tokenUtil.createRefreshToken(authUserDetails);
HashMap map=new HashMap<>();
map.put("accessToken",accessToken);
map.put("refreshToken",refreshToken);
ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.OK,"登录成功",map));
}
}
登入失败处理器
package com.lhm.springcloud.security.handler;
import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.utils.ResUtil;
import com.lhm.springcloud.security.utils.ResponseUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* @ClassName LoginFailureHandler
* @Description 登陆失败处理过滤器
* @Author liuheming
* @Date 2019/5/7 9:05
* @Version 1.0
**/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
//#限制用户登陆错误次数(次)
@Value("${security.loginTimeLimit}")
private Integer loginTimeLimit;
//#错误超过次数后多少分钟后才能继续登录(分钟)
@Value("${security.loginAfterTime}")
private Integer loginAfterTime;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* @Author liuheming
* @Description 用户登陆失败处理类 记录用户登陆错误次数
* @Date 9:12 2019/5/7
* @Param [request, response, e]
* @return void
**/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
String username = request.getParameter("username");
recordLoginTime(username);
String key = "loginTimeLimit:" + username;
String value = redisTemplate.opsForValue().get(key);
if (StringUtils.isBlank(value)) {
value = "0";
}
//获取已登录错误次数
int loginFailTime = Integer.parseInt(value);
int restLoginTime = loginTimeLimit - loginFailTime;
ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, "用户名或密码错误"));
} else if (e instanceof DisabledException) {
ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, "账户被禁用,请联系管理员"));
} else {
ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, "登录失败"));
}
}
/**
* 判断用户登陆错误次数
*/
public boolean recordLoginTime(String username) {
String key = "loginTimeLimit:" + username;
String flagKey = "loginFailFlag:" + username;
String value = redisTemplate.opsForValue().get(key);
if (StringUtils.isBlank(value)) {
value = "0";
}
//获取已登录错误次数
int loginFailTime = Integer.parseInt(value) + 1;
redisTemplate.opsForValue().set(key, String.valueOf(loginFailTime), loginAfterTime, TimeUnit.MINUTES);
if (loginFailTime >= loginTimeLimit) {
redisTemplate.opsForValue().set(flagKey, "fail", loginAfterTime, TimeUnit.MINUTES);
return false;
}
return true;
}
}
在登入的过程中会对用户的请求间隔时间及失败次数做记录。
鉴权的过程分成了两个大的步骤
第一对请求的路径、方法、头部信息进行判断,确认该请求是否需要鉴权
JWTAuthenticationFilter
package com.lhm.springcloud.security.filter;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.lhm.springcloud.security.constant.IgnoredUrlsProperties;
import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.constant.SecurityConstant;
import com.lhm.springcloud.security.exception.CommonException;
import com.lhm.springcloud.security.pojo.MyGrantedAuthority;
import com.lhm.springcloud.security.utils.ResUtil;
import com.lhm.springcloud.security.utils.ResponseUtil;
import com.lhm.springcloud.security.utils.SpringUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
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.ArrayList;
import java.util.List;
/**
* JWT过滤器1
*/
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
super(authenticationManager, authenticationEntryPoint);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
IgnoredUrlsProperties ignoredUrlsProperties= SpringUtil.getBean("ignoredUrlsProperties", IgnoredUrlsProperties.class);
String Requesturl=request.getRequestURI();
PathMatcher pathMatcher = new AntPathMatcher();
if(null != ignoredUrlsProperties){
for(String url:ignoredUrlsProperties.getUrls()){
if(pathMatcher.match(url,Requesturl)){
chain.doFilter(request, response);
return;
}
}
}
//获取请求头
String header = request.getHeader(SecurityConstant.HEADER);
//如果请求头中不存在 或 格式不对 则进入下个过滤器
if (StringUtils.isBlank(header) || !header.startsWith(SecurityConstant.TOKEN_SPLIT)) {
chain.doFilter(request, response);
return;
}
try {
UsernamePasswordAuthenticationToken authentication = getAuthentication(request, response);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, e.getMessage()));
return;
}
chain.doFilter(request, response);
}
/**
* @Author liuheming
* @Description 对token进行解析认证
* @Date 11:11 2019/5/7
* @Param [request, response]
* @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
**/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request, HttpServletResponse response) throws CommonException {
String token = request.getHeader(SecurityConstant.HEADER);
if (StringUtils.isNotBlank(token)) {
// 解析token
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(SecurityConstant.tokenSigningKey)
.parseClaimsJws(token.replace(SecurityConstant.TOKEN_SPLIT, ""))
.getBody();
//获取用户名
String username = claims.getSubject();
//获取权限
List authorities = new ArrayList();
String authority = claims.get(SecurityConstant.AUTHORITIES).toString();
if (StringUtils.isNotBlank(authority)) {
JSONArray list=JSONArray.parseArray(authority);
for (int i=0;i
第二判断当前请求token是否有权访问当前请求地址
MyFilterSecurityInterceptor
package com.lhm.springcloud.security.filter;
import com.lhm.springcloud.security.manager.MyAccessDecisionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
/**
* 权限管理过滤器2
* 监控用户行为
* @author Exrickx
*/
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
public void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
}
@Override
public Class> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
具体的处理会放到MySecurityMetadataSource中去判断,不过我这里做了个小优化,将处理权限的业务统一放到了MyAccessDecisionManager下,减少点性能开销
MySecurityMetadataSource
package com.lhm.springcloud.security.manager;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
/**
* 权限资源管理器
* 为权限决断器提供支持
*
* @author Exrickx
*/
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
/**
* 此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
* 因为每一次来了请求,都先要匹配一下权限表中的信息是不是包含此url,
* 因此优化一下,对url直接拦截,不管请求的url 是什么都直接拦截,然后在MyAccessDecisionManager的decide 方法中做拦截还是放行的决策。
* 所以此方法的返回值不能返回 null 此处随便返回一下。
*
* @param o
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection getAttributes(Object o) throws IllegalArgumentException {
Collection co = new ArrayList<>();
co.add(new SecurityConfig("null"));
return co;
}
@Override
public Collection getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class> aClass) {
return true;
}
}
MyAccessDecisionManager
package com.lhm.springcloud.security.manager;
import com.lhm.springcloud.security.pojo.MyGrantedAuthority;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
/**
* @ClassName MyAccessDecisionManager
* @Description 权限最终判断器
* * 判断用户拥有的角色是否有资源访问权限
* @Author liuheming
* @Date 2019/5/7 10:44
* @Version 1.0
**/
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
//decide 方法是判定是否拥有权限的决策方法
@Override
public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
String url, method;
AntPathRequestMatcher matcher;
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (ga instanceof MyGrantedAuthority) {
MyGrantedAuthority urlGrantedAuthority = (MyGrantedAuthority) ga;
url = urlGrantedAuthority.getPermissionUrl();
method = urlGrantedAuthority.getMethod();
matcher = new AntPathRequestMatcher(url);
if (matcher.matches(request)) {
//当权限表权限的method为ALL时表示拥有此路径的所有请求方式权利。
if (method.equals(request.getMethod()) || "ALL".equals(method)) {
return;
}
}
}
throw new AccessDeniedException("您没有访问权限");
}
throw new AccessDeniedException("鉴权出错");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class> clazz) {
return true;
}
}
decide()方法中的MyGrantedAuthority是我自定义的权限对象 因为原有的SimpleGrantedAuthority类只有一个属性,无法完成RESTfull风格的请求。
MyGrantedAuthority
package com.lhm.springcloud.security.pojo;
import org.springframework.security.core.GrantedAuthority;
/**
* @ClassName MyGrantedAuthority
* @Description TODO
* @Author liuheming
* @Date 2019/5/7 10:39
* @Version 1.0
**/
public class MyGrantedAuthority implements GrantedAuthority {
private String url;
private String method;
public String getPermissionUrl() {
return url;
}
public void setPermissionUrl(String permissionUrl) {
this.url = permissionUrl;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public MyGrantedAuthority(String url, String method) {
this.url = url;
this.method = method;
}
@Override
public String getAuthority() {
return this.url + ";" + this.method;
}
}
最后将我们自定义的类全部注入到security提供的配置文件类中,具体的配置我都用注解表明了。
package com.lhm.springcloud.security.config;
import com.lhm.springcloud.security.constant.IgnoredUrlsProperties;
import com.lhm.springcloud.security.filter.JWTAuthenticationFilter;
import com.lhm.springcloud.security.filter.MyFilterSecurityInterceptor;
import com.lhm.springcloud.security.filter.WebSecurityCorsFilter;
import com.lhm.springcloud.security.handler.RestAccessDeniedHandler;
import com.lhm.springcloud.security.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
/*
* Security 核心配置类
* 开启控制权限至Controller
* @author Exrickx
* */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private IgnoredUrlsProperties ignoredUrlsProperties;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private AuthenticationSuccessHandler successHandler;
@Autowired
private AuthenticationFailureHandler failHandler;
@Autowired
private RestAccessDeniedHandler accessDeniedHandler;
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
//密码加密使用 Spring Security 提供的BCryptPasswordEncoder.encode(user.getRawPassword().trim())
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = http
.authorizeRequests();
//除配置文件忽略路径其它所有请求都需经过认证和授权
for(String url:ignoredUrlsProperties.getUrls()){
registry.antMatchers(url).permitAll();
}
registry.antMatchers(HttpMethod.OPTIONS).permitAll()
.and()
//表单登录方式
.formLogin()
.loginPage("/login/needLogin")
//登录需要经过的url请求
.loginProcessingUrl("/api/v1/auth/login")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
//成功处理类
.successHandler(successHandler)
//失败
.failureHandler(failHandler)
.and()
.logout()
.permitAll()
.and()
.authorizeRequests()
//任何请求
.anyRequest()
//需要身份认证
.authenticated()
.and()
//关闭跨站请求防护
.csrf().disable()
//前后端分离采用JWT 不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//自定义权限拒绝处理类
.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and()
//添加自定义权限过滤器
.addFilterBefore(new WebSecurityCorsFilter(), ChannelProcessingFilter.class)
.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class)
//添加JWT过滤器 除/login其它请求都需经过此过滤器
.addFilter(new JWTAuthenticationFilter(authenticationManager()));
}
}
以上就是最核心的代码部分,完整代码已经贴出,有兴趣的同学可以结合代码学习一下。
git:https://github.com/liuheming/springcloudDemo.git