上篇我們通過掃描注解自動(dòng)生成了菜單海蔽,本篇我們通過Spring Security來加上權(quán)限驗(yàn)證稍走。
接上篇 http://www.reibang.com/p/ef5854cce4eb
Spring Security配置
Spring Security 是Spring提供的安全控制組件睛廊,它本身提供了很多功能躁锁,我們目前用不到崔挖,Spring Security支持通過配置來啟用串塑、禁用這些功能。本文打算實(shí)現(xiàn)基于Token的認(rèn)證娃承、授權(quán)模式奏夫,先對Spring Security進(jìn)行配置。
Spring Security的詳細(xì)使用可以在網(wǎng)上搜索相關(guān)資料历筝。
@Configuration
@EnableWebSecurity
public class MVCWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();
http
.exceptionHandling()
//未登錄時(shí)的handler
.authenticationEntryPoint(new UnauthenticatedEntryPoint())
//無權(quán)限時(shí)的handler
.accessDeniedHandler(new UnauthorizedAccessDeniedHandler())
.and()
//如果是微服務(wù) 需要禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//不關(guān)心跨域
.csrf().disable()
//我們自己實(shí)現(xiàn)匿名
.anonymous().disable()
//我們自己實(shí)現(xiàn)登錄
.formLogin().disable()
//我們自己實(shí)現(xiàn)注銷
.logout().disable()
//業(yè)務(wù)邏輯
.addFilterAt(new TokenAuthenticationFilter(), RememberMeAuthenticationFilter.class)
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
}
核心部分
登錄和鑒權(quán)
我們先實(shí)現(xiàn)一個(gè) Token Principal類酗昼,這個(gè)類最終也是 JSP的 userPrincipal
。
public class AuthorityAuthenticationToken extends AbstractAuthenticationToken {
private String principal;
public AuthorityAuthenticationToken(String principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
setAuthenticated(true);
}
public AuthorityAuthenticationToken(String principal) {
super(null);
this.principal = principal;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null; //不支持密碼
}
@Override
public Object getPrincipal() {
return principal;
}
}
然后聲明兩個(gè)接口梳猪,分別用于認(rèn)證和鑒權(quán)麻削。
public interface ITokenProvider extends Ordered {
String getToken(HttpServletRequest request);
}
public interface ITokenConverter extends Ordered {
AuthorityAuthenticationToken decodeToken(String token);
}
這兩個(gè)接口繼承了org.springframework.core.Ordered
接口,然后我們實(shí)現(xiàn)一個(gè)過濾器來進(jìn)行登錄。
public class TokenAuthenticationFilter implements Filter {
private ITokenProvider[] providers;
private ITokenConverter[] converters;
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
private final AnonymousAuthenticationToken anonymousUser = new AnonymousAuthenticationToken(
UUID.randomUUID().toString(), "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")
);
private String getToken(HttpServletRequest request) {
if (providers == null) { //冪等的 所以沒加鎖
providers = SpringApplicationContextHolder.getInstance()
.getBeansOfType(ITokenProvider.class)
.values().stream()
.sorted(Comparator.comparingInt(ITokenProvider::getOrder)) //有序
.toArray(ITokenProvider[]::new);
}
for (ITokenProvider provider : providers) {
String token = provider.getToken(request);
if (StringUtils.hasText(token)) //取第一個(gè)成功的
return token;
}
return null;
}
private AuthorityAuthenticationToken decodeToken(String token) {
if (StringUtils.isEmpty(token))
return null;
if (converters == null) {
converters = SpringApplicationContextHolder.getInstance()
.getBeansOfType(ITokenConverter.class)
.values().stream()
.sorted(Comparator.comparingInt(ITokenConverter::getOrder)) //有序
.toArray(ITokenConverter[]::new);
}
for (ITokenConverter converter : converters) {
AuthorityAuthenticationToken u = converter.decodeToken(token);
if (u != null) //取第一個(gè)成功的
return u;
}
return null;
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
try {
HttpServletRequest request = (HttpServletRequest) req;
String token = getToken(request);
AuthorityAuthenticationToken authToken = null;
if (StringUtils.hasText(token)) {
authToken = decodeToken(token);
}
if (authToken != null) {
authToken.setDetails(authenticationDetailsSource.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
} else {
SecurityContextHolder.getContext().setAuthentication(anonymousUser);
}
} catch (Exception e) {
SecurityContextHolder.getContext().setAuthentication(anonymousUser);
}
chain.doFilter(req, res);
}
@Override
public void destroy() {
}
}
這里允許 TokenProvider
和TokenConverter
同時(shí)存在多份碟婆,以便在同一個(gè)服務(wù)中支持多種授權(quán)方式电抚。
權(quán)限校驗(yàn)
Spring Security的權(quán)限校驗(yàn)是投票機(jī)制的,默認(rèn)實(shí)現(xiàn)了一票否決制(只要有一票不允許就不能訪問)竖共、一票允許值(與否決相對的蝙叛,只要有一票允許就可以訪問)、多票優(yōu)勝公给。Spring Security默認(rèn)采用的是一票允許值借帘,我們這里采用一票否決制。
此外Spring Security的權(quán)限校驗(yàn)是基于注解的淌铐,我們希望能基于我們的菜單注解肺然,所以需要在投票之前進(jìn)行注解數(shù)據(jù)轉(zhuǎn)換,在這里我們采用一種簡單粗暴的辦法腿准,用一個(gè)Wrapper把投票器包起來际起,在Wrapper中進(jìn)行數(shù)據(jù)轉(zhuǎn)換。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MVCMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected AccessDecisionManager accessDecisionManager() {
AbstractAccessDecisionManager decisionManager = (AbstractAccessDecisionManager)super.accessDecisionManager();
//wrapper和一票否決 雖然其實(shí)只有一票
return new AccessDecisionManagerWrapper(new UnanimousBased(decisionManager.getDecisionVoters()));
}
}
Wrapper類
public class AccessDecisionManagerWrapper implements AccessDecisionManager {
private AbstractAccessDecisionManager wrapped;
private Logger logger = LoggerFactory.getLogger(AccessDecisionManagerWrapper.class);
private AuthorizeAttributeLoader loader = new AuthorizeAttributeLoader();
public AccessDecisionManagerWrapper(AbstractAccessDecisionManager wrapped) {
this.wrapped = wrapped;
}
private boolean isAnonymous(Authentication authentication) {
return (authentication instanceof AnonymousAuthenticationToken);
}
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if (authentication == null || !authentication.isAuthenticated()) { //啥信息都沒有
wrapped.decide(authentication, object, configAttributes);
} else {
//注解數(shù)據(jù)轉(zhuǎn)換
configAttributes = loader.loadAuthorizeAttribute(object, configAttributes);
wrapped.decide(authentication, object, configAttributes);
}
}
@Override
public boolean supports(ConfigAttribute attribute) {
return wrapped.supports(attribute);
}
@Override
public boolean supports(Class<?> clazz) {
return wrapped.supports(clazz);
}
}
注解轉(zhuǎn)換部分吐葱,我們先聲明一個(gè)IAuthorizeAttributeProvider
接口街望,以便擴(kuò)展到其他功能。
public interface IAuthorizeAttributeProvider {
void loadAttribute(Class<?> aClass, Method method, AuthorizeAttributeContainer container);
}
public class AuthorizeAttributeLoader {
//理論上運(yùn)行過程中 這個(gè)是不會變化的 緩存起來
private Map<MethodInvocation, Collection<ConfigAttribute>> attrCache = new ConcurrentHashMap<>();
private Class<?> getTargetClass(MethodInvocation mi) {
Object target = mi.getThis();
if (target != null) {
return target instanceof Class<?> ? (Class<?>) target
: AopProxyUtils.ultimateTargetClass(target);
}
return null;
}
public Collection<ConfigAttribute> loadAuthorizeAttribute(Object object, Collection<ConfigAttribute> configAttributes) {
if (object instanceof MethodInvocation) {
MethodInvocation mi = (MethodInvocation) object;
Collection<ConfigAttribute> result = attrCache.get(mi);
if (result == null) {
Method method = mi.getMethod();
AuthorizeAttributeContainer container = new AuthorizeAttributeContainer(configAttributes);
SpringApplicationContextHolder.getInstance()
.getBeansOfType(IAuthorizeAttributeProvider.class)
.forEach((k, v) -> v.loadAttribute(getTargetClass(mi), method, container));
result = new ArrayList<>(container.getAttributes());
attrCache.put(mi, result);
}
return result;
}
return configAttributes;
}
}
核心功能部分的操作都是冪等的弟跑,所以沒有加鎖灾前,最后我們再實(shí)現(xiàn)未登錄和權(quán)限校驗(yàn)失敗的監(jiān)聽。
//這個(gè)類在MVCWebSecurityConfig注冊的
public class UnauthenticatedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setStatus(HttpStatus.OK.value());
//未登錄孟辑,跳轉(zhuǎn)到/401
RequestDispatcher dispatcher = request.getRequestDispatcher(
"/401?redirect=" + response.encodeRedirectURL(request.getRequestURI())
);
dispatcher.forward(request, response);
}
//這個(gè)類在MVCWebSecurityConfig注冊的
public class UnauthorizedAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setStatus(HttpStatus.OK.value());
//沒權(quán)限哎甲,跳轉(zhuǎn)到403
RequestDispatcher dispatcher = request.getRequestDispatcher(
"/403?redirect=" + response.encodeRedirectURL(request.getRequestURI())
);
dispatcher.forward(request, response);
}
}
菜單部分
實(shí)現(xiàn)了核心部分后,我們基于核心部分?jǐn)U展修改菜單部分饲嗽。
注解
首先炭玫,在注解中增加權(quán)限碼。
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@PreAuthorize("isAuthenticated()") //這個(gè)注解必須有喝噪,啟用Spring Security的投票機(jī)制
public @interface MenuItem {
String label();
String icon();
String code(); //權(quán)限碼
}
然后實(shí)現(xiàn)菜單注解到Spring Security的注解的轉(zhuǎn)換础嫡。
@Component
public class MenuAuthorizeAttributeProvider implements IAuthorizeAttributeProvider {
private ExpressionBasedAnnotationAttributeFactory attributeFactory = ExpressionAttrFactoryManager.getFactory();
@Override
public void loadAttribute(Class<?> aClass, Method method, AuthorizeAttributeContainer container) {
MenuItem menuItem = method.getDeclaredAnnotation(MenuItem.class);
if (menuItem == null)
return;
String code = menuItem.code();
if ("*".equals(code))
return;
//實(shí)現(xiàn)一個(gè)擴(kuò)展,管理員有所有菜單的權(quán)限
String exp;
if ("admin".equals(code)) {
exp = "hasAuthority('admin')";
} else {
//有菜單權(quán)限或是管理員
exp = "hasAuthority('" + code + "') || hasAuthority('admin')";
}
container.addAttribute(
attributeFactory.createPreInvocationAttribute(null, null, exp)
);
}
}
菜單服務(wù)
擴(kuò)展菜單服務(wù)酝惧,能過濾當(dāng)前用戶能看到的菜單列表榴鼎,由于菜單服務(wù)沒有權(quán)限的信息,所以我們需要聲明一個(gè)接口晚唇,由權(quán)限服務(wù)來實(shí)現(xiàn)巫财。
public interface IUserAuthorityProvider {
Set<String> getCurrentUserAuthorities();
}
//菜單服務(wù)的主要修改部分
public List<MenuVO> getUserMenu() {
Set<String> authorities = provider.getCurrentUserAuthorities();
if (authorities.isEmpty())
return Collections.emptyList();
List<MenuVO> menu = getAllMenu();
if (authorities.contains("admin"))
return menu; //管理員能看到所有菜單
return menu.stream()
.filter(m -> authorities.contains(m.getCode()))
.collect(Collectors.toList());
}
測試
核心部分和菜單部分分別暴漏了3個(gè)接口,我們依次實(shí)現(xiàn)這3個(gè)接口哩陕。
登錄驗(yàn)證
@Component
public class UrlParameterTokenProvider implements ITokenProvider {
@Override
public String getToken(HttpServletRequest request) {
//這里應(yīng)該是從cookie或http頭獲取用戶是否登錄平项,token是否有效
//單純測試赫舒,我們從URL獲取
return request.getParameter("token");
}
@Override
public int getOrder() {
return 0;
}
}
鑒權(quán)
@Component
public class TestTokenDecoder implements ITokenConverter {
@Override
public AuthorityAuthenticationToken decodeToken(String token) {
if (StringUtils.isEmpty(token))
return null;
//這里是加載用戶有哪些權(quán)限
List<GrantedAuthority> authorities = new ArrayList<>(2);
switch (token) {
case "1":
//菜單1的權(quán)限
authorities.add(new SimpleGrantedAuthority("test1"));
break;
case "2":
//菜單2的權(quán)限
authorities.add(new SimpleGrantedAuthority("test2"));
break;
case "admin":
authorities.add(new SimpleGrantedAuthority("admin"));
break;
}
return new AuthorityAuthenticationToken(token, authorities);
}
@Override
public int getOrder() {
return 0;
}
}
加載當(dāng)前用戶的權(quán)限
@Component
public class TestUserAuthoritiesProvider implements IUserAuthorityProvider {
@Override
public Set<String> getCurrentUserAuthorities() {
//正常應(yīng)該有業(yè)務(wù)提供這個(gè)數(shù)據(jù),暫時(shí)從Principal獲取
HttpServletRequest request = ServletHolder.getCurrentRequest();
Principal principal = request.getUserPrincipal();
if (principal == null) //未登錄
return Collections.emptySet();
if (!(principal instanceof AbstractAuthenticationToken)) {
return Collections.emptySet();
}
Collection<GrantedAuthority> authorities = ((AbstractAuthenticationToken) principal).getAuthorities();
return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
}
}
錯(cuò)誤頁面
public class ErrorController {
@RequestMapping("/401")
@ResponseBody
public String on401(String redirect) {
//應(yīng)該顯示登錄界面
return "請登錄后在訪問";
}
@RequestMapping("/403")
@ResponseBody
public String on403(String redirect) {
return "您沒有權(quán)限訪問此功能";
}
}