我在使用spring進行開發時,一般是使用 aop+jwt 模式來對調用者身份進行確認。前幾天接觸到一個開源商城源碼(github地址)裏面使用spring security +jwt 來進行權限的驗證。可是源碼中只實現了簡單的用戶名密碼驗證,關於權限的略過了。雖然之前瞭解過spring security可是沒有實際使用過,藉着這個機會整合了一下spring security jwt。(能夠拿起就用:smirk:) github源碼地址css
spring security 是基於spring的一個web安全框架。通常來講,web應用的安全性包括用戶認證和用戶受權兩個部分。用戶認證常見的就是用戶名密碼驗證。用戶受權則指的是查看用戶是否有權限調用資源。html
對於用戶認證,咱們自定義的話一般須要本身實現 PasswordEncoder UserDetail兩個類。PasswordEncoder主要實現了密碼的加密,以及密碼的比較(登錄時用戶密碼與數據庫存儲的密碼)。我實現的PasswordEncoder代碼以下java
package com.lichaobao.springsecurityjwt.component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.password.PasswordEncoder;
/** * @author lichaobao * @date 2018/12/22 * @QQ 1527563274 */
public class MyPasswordEncoder implements PasswordEncoder {
private static final Logger LOGGER = LoggerFactory.getLogger(MyPasswordEncoder.class);
/** * 自定義密碼加密(出於示例,本代碼沒有對密碼進行加密,直接返回原密碼) * @param charSequence 須要加密的密碼 * @return 加密後的密碼 */
@Override
public String encode(CharSequence charSequence) {
LOGGER.info("now encode password :{}",charSequence.toString());
return charSequence.toString();
}
/** * 比較加密後的密碼與數據庫中的密碼是否匹配 * @param charSequence 用戶登錄傳來的密碼 * @param s 數據庫中存儲的密碼 * @return true 匹配 false 不匹配 */
@Override
public boolean matches(CharSequence charSequence, String s) {
LOGGER.info("matchs charSequence :{} and password :{}",charSequence,s);
return encode(charSequence).equals(s);
}
}
複製代碼
對於UserDetails類來講主要起到了封裝用戶信息的做用,包括用戶的基本信息以及擁有的權限信息的封裝。默認UserDetails的生成類是 UserDetailsService。 這個接口中提供了loadUserByUsername(String username)方法UserDetailService源碼以下git
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
複製代碼
在本例中 咱們經過修改WebSecurityConfigAdapter中的UserdetailsServices實現代碼以下github
/** * 在次代碼中完成用戶基本信息的查詢好比用戶名 密碼 權限等封裝後 返回 * 此方法的入口 爲 userDetailsService.loadUserByUsername(String username) * @return UserDetail */
@Bean
@Override
protected UserDetailsService userDetailsService() {
return username ->{
if(users.containsKey(username)){
return new MyUserDetails(username,users.get(username),permissions.get(username));
}
throw new UsernameNotFoundException("用戶名錯誤");
};
}
複製代碼
自定義權限驗證咱們經過自定義AccesDecisionVoter類來實現。關鍵代碼以下web
/** * @author lichaobao * @date 2018/12/22 * @QQ 1527563274 */
public class RoleBasedVotor implements AccessDecisionVoter {
private static final Logger LOGGER = LoggerFactory.getLogger(RoleBasedVotor.class);
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;//根據本身的邏輯修改 不能直接return false不然驗證不經過
}
/** * 主要驗證邏輯 * ROLE_ANONYMOUS 表明全部人能夠訪問 這是 spring security 自動生成的 能夠自定義 * @param authentication 用戶信息 * @param o 能夠從這裏拿到url * @param collection 訪問資源須要的權限在本例中因爲咱們將url做爲驗證依據因此爲用到collection * @return ACCESS_DENIED(-1)無權限 ACCESS_GRANTED(1)有權限 */
@Override
public int vote(Authentication authentication, Object o, Collection collection) {
FilterInvocation fi = (FilterInvocation) o;
String url = fi.getRequestUrl();
LOGGER.info("url :{}",url);
if(authentication == null){
return ACCESS_DENIED;
}
Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
Iterator iterator = authorities.iterator();
while (iterator.hasNext()){
GrantedAuthority ga = (GrantedAuthority) iterator.next();
LOGGER.info(ga.getAuthority());
if(equalsurl(url,ga.getAuthority())||"ROLE_ANONYMOUS".equals(ga.getAuthority())){
return ACCESS_GRANTED;
}
}
return ACCESS_DENIED;
}
@Override
public boolean supports(Class aClass) {
return true;//根據本身的邏輯修改 不能直接return false不然驗證不經過
}
/** * 得到用戶權限信息 * @param authentication * @return */
Collection<? extends GrantedAuthority> extractAuthorities(
Authentication authentication) {
LOGGER.info("extractAuthorites:{}",authentication.getAuthorities());
return authentication.getAuthorities();
}
/** * 比較權限 權限 /** 表明 如下全部能訪問 /* 表明如下一級能訪問 如 用戶權限爲 /test/** 則能訪問 /test/a /test/b/c * 如用戶權限爲 /test/* 則能訪問 /test/a 而 /test/b/c則不能訪問 * @param url 訪問的url * @param urlpermission 擁有的權限 * @return boolean */
static boolean equalsurl(String url,String urlpermission) {
url = url.startsWith("/") ? url.substring(1):url;
urlpermission = urlpermission.startsWith("/")?urlpermission.substring(1):urlpermission;
if("**".equals(urlpermission)){
return true;
}else if("*".equals(urlpermission)){
return url.split("/").length == 1;
}
else if(urlpermission.endsWith("/**")){
String afterUrl = urlpermission.substring(0,urlpermission.length()-3);
return url.startsWith(afterUrl);
}else if(urlpermission.endsWith("/*")){
String afterUrl = urlpermission.substring(0,urlpermission.length()-2);
String[] urlPiece = url.split("/");
return url.startsWith(afterUrl)&&urlPiece.length == 2;
}
return url.equals(urlpermission);
}
}
複製代碼
在登錄邏輯的代碼中,咱們須要經過登錄接口接收到的用戶名、密碼生成UsernamePasswordAuthenticationToken 爲接下來的驗證提供一個橋樑。注意密碼要根據自定義實現的PasswordEncoder中的加密方法或着其餘本身實現的加密方法進行加密要保證加密後和數據庫中的密碼相對應。而後 經過調用authenticationManager中的authenticate方法進行驗證。具體代碼以下spring
@Service
public class SignServiceImpl implements SignService {
private static final Logger LOGGER = LoggerFactory.getLogger(SignService.class);
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserDetailsService userDetailsService;
@Autowired
JwtUtils jwtUtils;
@Autowired
PasswordEncoder passwordEncoder;
/** * 登錄 登錄出現錯誤拋出錯誤 用catch接受便可 * @param username 用戶名 * @param password 密碼 * @return String token */
@Override
public String login(String username, String password) {
String token = null;
/** * 封裝 注意密碼加密 */
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username,passwordEncoder.encode(password));
try{
Authentication authentication = authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
/** * 加載數據庫中的用戶名密碼 主要邏輯爲UserdetailsServices中的代碼 */
userDetailsService.loadUserByUsername(username);
token = jwtUtils.generateToken(username);
}catch (Exception e){
e.printStackTrace();
LOGGER.info("認證失敗 :{}",e.getMessage());
}
return token;
}
}
複製代碼
具體實現爲繼承OncePerRequestFilter方法實現本身的Filter ,經過解析token得到用戶信息,而後比對用戶權限。代碼以下:數據庫
/** * @author lichaobao * @date 2018/12/22 * @QQ 1527563274 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
JwtUtils jwtUtils;
@Autowired
UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if(token != null && SecurityContextHolder.getContext().getAuthentication() == null){
String username = jwtUtils.getUserNameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
LOGGER.info("UserDetails :{},permissions:{}",userDetails.getUsername(),userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
LOGGER.info("authenticated user :{}",username);
LOGGER.info("already filter name:{}",super.getAlreadyFilteredAttributeName());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request,response);
}
}
複製代碼
具體實現爲繼承WebSecurityConfigurerAdapter方法 根據本身的須要重寫邏輯api
/** * @author lichaobao * @date 2018/12/22 * @QQ 1527563274 */
@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyAccessDeineHandler myAccessDeineHandler;
@Autowired
MyAuthenticationEntryPoint myAuthenticationEntryPoint;
/** * 模擬數據庫用戶 */
private static Map<String,String> users;
/** * 模擬權限 */
private static Map<String,List<String>> permissions;
static {
users = new HashMap<>();
permissions = new HashMap<>();
users.put("a","a");
String[] aper = new String[]{"/a/**","/test/all"};
permissions.put("a",Arrays.asList(aper));
users.put("b","b");
String[] bper = new String[]{"/b/**","test/all"};
permissions.put("b",Arrays.asList(bper));
users.put("admin","password");
String[] adminPer = new String[]{"/**"};
permissions.put("admin",Arrays.asList(adminPer));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()//禁用csrf 由於使用jwt不須要
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)//禁用session
.and()
.authorizeRequests()
.accessDecisionManager(accessDecisionManager())//加載本身的accessDecisionManager 用到了RoleBasedVotor
.antMatchers(HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources/**",
"/v2/api-docs/**")
.permitAll()//容許訪問全部界面資源
.antMatchers("/login","/register")
.permitAll()//容許訪問登錄註冊接口 而後 "/login"與"/register"的權限爲"ROLE_ANONYMOUS"
.antMatchers(HttpMethod.OPTIONS)
.permitAll()//跨域請求 會有一個OPTIONS 請求 所有容許
.anyRequest()//其餘任何都須要驗證
.authenticated();
/** * 禁用緩存 */
http.headers().cacheControl();
/** * 配置自定義的Filter */
http.addFilterBefore(jwtAuthenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class);
/** * 配置自定義的無權限以及用戶名密碼錯誤返回結果 */
http.exceptionHandling()
.accessDeniedHandler(myAccessDeineHandler)
.authenticationEntryPoint(myAuthenticationEntryPoint);
}
/** * 配置 userDetailsService 以及passwordEncoder; * @param auth * @throws Exception */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
/** * 在次代碼中完成用戶基本信息的查詢好比用戶名 密碼 權限等封裝後 返回 * 此方法的入口 爲 userDetailsService.loadUserByUsername(String username) * @return UserDetail */
@Bean
@Override
protected UserDetailsService userDetailsService() {
return username ->{
if(users.containsKey(username)){
return new MyUserDetails(username,users.get(username),permissions.get(username));
}
throw new UsernameNotFoundException("用戶名錯誤");
};
}
@Bean
public PasswordEncoder passwordEncoder(){
return new MyPasswordEncoder();
}
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
return new JwtAuthenticationTokenFilter();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/** * 具體使用RoleBasedVotor方法 * @return */
@Bean
public AccessDecisionManager accessDecisionManager(){
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new WebExpressionVoter(),
new RoleBasedVotor(),
new AuthenticatedVoter());
return new UnanimousBased(decisionVoters);
}
}
複製代碼