SpringCloud之Security

Spring Security是Spring提供的一個安全框架,提供認證和授權功能,最主要的是它提供了簡單的使用方式,同時又有很高的靈活性,簡單,靈活,強大。

我個人博客系統採用的權限框架就是Spring Security,正好整合到SpringCloud裏面。

一般系統里關於角色方面通常有這麼幾張表,角色表、用戶-角色表、菜單表、角色-菜單表等。

不過我個人博客系統主要以wordpress作為參考,沿用其12張表,如圖:

 

 

一、導入Maven依賴

<properties>
       <jjwt.version>0.9.0</jjwt.version>
       <spring-security-jwt.version>1.0.9.RELEASE</spring-security-jwt.version>
</properties>
 <!-- springsecurity-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-security</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.security</groupId>
           <artifactId>spring-security-jwt</artifactId>
           <version>${spring-security-jwt.version}</version>
       </dependency>
       <dependency>
           <groupId>io.jsonwebtoken</groupId>
           <artifactId>jjwt</artifactId>
           <version>${jjwt.version}</version>
       </dependency>

二、編寫Spring Security配置類

package com.springcloud.blog.admin.config;
import com.springcloud.blog.admin.security.UserAuthenticationProvider;
import com.springcloud.blog.admin.security.UserPermissionEvaluator;
import com.springcloud.blog.admin.security.handler.*;
import com.springcloud.blog.admin.security.jwt.JWTAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;

/**
 * SpringSecurity配置類
 * @Author youcong
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //開啟權限註解,默認是關閉的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 自定義登錄成功處理器
     */
    @Autowired
    private UserLoginSuccessHandler userLoginSuccessHandler;
    /**
     * 自定義登錄失敗處理器
     */
    @Autowired
    private UserLoginFailureHandler userLoginFailureHandler;
    /**
     * 自定義註銷成功處理器
     */
    @Autowired
    private UserLogoutSuccessHandler userLogoutSuccessHandler;
    /**
     * 自定義暫無權限處理器
     */
    @Autowired
    private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
    /**
     * 自定義未登錄的處理器
     */
    @Autowired
    private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
    /**
     * 自定義登錄邏輯驗證器
     */
    @Autowired
    private UserAuthenticationProvider userAuthenticationProvider;

    /**
     * 加密方式
     * @Author youcong
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**
     * 注入自定義PermissionEvaluator
     */
    @Bean
    public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler(){
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setPermissionEvaluator(new UserPermissionEvaluator());
        return handler;
    }

    /**
     * 配置登錄驗證邏輯
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth){
        //這裏可啟用我們自己的登陸驗證邏輯
        auth.authenticationProvider(userAuthenticationProvider);
    }
    /**
     * 配置security的控制邏輯
     * @Author youcong
     * @Param  http 請求
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
                // 不進行權限驗證的請求或資源(從配置文件中讀取)
                .antMatchers(JWTConfig.antMatchers.split(",")).permitAll()
                // .antMatchers("/*").permitAll()
                // 其他的需要登陸后才能訪問
                .anyRequest().authenticated()
                .and()
                // 配置未登錄自定義處理類
                .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
                .and()
                // 配置登錄地址
                .formLogin()
                .loginProcessingUrl("/login/userLogin")
                // 配置登錄成功自定義處理類
                .successHandler(userLoginSuccessHandler)
                // 配置登錄失敗自定義處理類
                .failureHandler(userLoginFailureHandler)
                .and()
                // 配置登出地址
                .logout()
                .logoutUrl("/login/userLogout")
                // 配置用戶登出自定義處理類
                .logoutSuccessHandler(userLogoutSuccessHandler)
                .and()
                // 配置沒有權限自定義處理類
                .exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler)
                .and()
                // 開啟跨域
                .cors()
                .and()
                // 取消跨站請求偽造防護
                .csrf().disable();
        // 基於Token不需要session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 禁用緩存
        http.headers().cacheControl();
        // 添加JWT過濾器
        http.addFilter(new JWTAuthenticationTokenFilter(authenticationManager()));
    }
}

三、編寫JWTConfig和application.yml增加jwt相關配置

package com.springcloud.blog.admin.config;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * JWT配置類
 * @Author youcong
 */
@Getter
@Component
@ConfigurationProperties(prefix = "jwt")
public class JWTConfig {
    /**
     * 密鑰KEY
     */
    public static String secret;
    /**
     * TokenKey
     */
    public static String tokenHeader;
    /**
     * Token前綴字符
     */
    public static String tokenPrefix;
    /**
     * 過期時間
     */
    public static Integer expiration;
    /**
     * 不需要認證的接口
     */
    public static String antMatchers;


    public void setSecret(String secret) {
        this.secret = secret;
    }

    public void setTokenHeader(String tokenHeader) {
        this.tokenHeader = tokenHeader;
    }

    public void setTokenPrefix(String tokenPrefix) {
        this.tokenPrefix = tokenPrefix;
    }

    public void setExpiration(Integer expiration) {
        this.expiration = expiration * 1000;
    }

    public void setAntMatchers(String antMatchers) {
        this.antMatchers = antMatchers;
    }


}

application.yml增加如下內容:

# JWT配置
jwt:
  # 密匙KEY
  secret: JWTSecret
  # HeaderKEY
  tokenHeader: Authorization
  # Token前綴字符
  tokenPrefix: challenger-
  # 過期時間 單位秒 1天後過期=86400 7天後過期=604800
  expiration: 86400
  # 配置不需要認證的接口
  antMatchers: /index/**,/login/**,/favicon.ico
  # 有效時間
  validTime: 7

四、編寫過濾器處理類

1.UserLoginSuccessHandler.java

package com.springcloud.blog.admin.security.handler;


import com.springcloud.blog.admin.config.JWTConfig;
import com.springcloud.blog.admin.security.entity.SelfUserEntity;
import com.springcloud.blog.admin.utils.AccessAddressUtil;
import com.springcloud.blog.admin.utils.JWTTokenUtil;
import com.springcloud.blog.admin.utils.RedisUtil;
import com.springcloud.blog.admin.utils.ResultUtil;
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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description 登錄成功處理類
 * @Author youcong
 */
@Component
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {


    /**
     * 登錄成功返回結果
     * @Author youcong
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
        // 組裝JWT
        SelfUserEntity selfUserEntity =  (SelfUserEntity) authentication.getPrincipal();
        String token = JWTTokenUtil.createAccessToken(selfUserEntity);
        token = JWTConfig.tokenPrefix + token;

        // 封裝返回參數
        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","200");
        resultData.put("msg", "登錄成功");
        resultData.put("token",token);
        ResultUtil.responseJson(response,resultData);
    }
}

2.UserLoginFailureHandler.java

package com.springcloud.blog.admin.security.handler;


import com.springcloud.blog.admin.utils.ResultUtil;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Description 登錄失敗處理類
 * @Author youcong
 */
@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {
    /**
     * 登錄失敗返回結果
     * @Author youcong
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){
        // 這些對於操作的處理類可以根據不同異常進行不同處理
        if (exception instanceof UsernameNotFoundException){
            System.out.println("【登錄失敗】"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用戶名不存在"));
        }
        if (exception instanceof LockedException){
            System.out.println("【登錄失敗】"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用戶被凍結"));
        }
        if (exception instanceof BadCredentialsException){
            System.out.println("【登錄失敗】"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"密碼錯誤"));
        }
        ResultUtil.responseJson(response,ResultUtil.resultCode(500,"登錄失敗"));
    }
}

3.UserLogoutSuccessHandler.java

package com.springcloud.blog.admin.security.handler;


import com.springcloud.blog.admin.utils.DateUtil;
import com.springcloud.blog.admin.utils.RedisUtil;
import com.springcloud.blog.admin.utils.ResultUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * 登出成功處理類
 * @Author youcong
 */
@Component
public class UserLogoutSuccessHandler implements LogoutSuccessHandler {
    

    /**
     * 用戶登出返回結果
     * 這裏應該讓前端清除掉Token
     * @Author youcong
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){

        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","200");
        resultData.put("msg", "登出成功");
        SecurityContextHolder.clearContext();
        ResultUtil.responseJson(response,ResultUtil.resultSuccess(resultData));
    }
}

4.UserAuthAccessDeniedHandler.java

package com.springcloud.blog.admin.security.handler;
import com.springcloud.blog.admin.utils.ResultUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Description 暫無權限處理類
 * @Author youcong
 */
@Component
public class UserAuthAccessDeniedHandler implements AccessDeniedHandler {
    /**
     * 暫無權限返回結果
     * @Author youcong
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception){
        ResultUtil.responseJson(response,ResultUtil.resultCode(403,"未授權"));
    }
}

5.UserAuthenticationEntryPointHandler.java

package com.springcloud.blog.admin.security.handler;


import com.springcloud.blog.admin.utils.ResultUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 用戶未登錄處理類
 * @Author youcong
 */
@Component
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
    /**
     * 用戶未登錄返回結果
     * @Author youcong
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){
        ResultUtil.responseJson(response,ResultUtil.resultCode(401,"未登錄"));
    }
}

6.UserAuthenticationProvider.java

自定義登錄驗證這個類,需要根據實際情況重寫。通常來說改動不大。

package com.springcloud.blog.admin.security;

import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.springcloud.blog.admin.entity.Usermeta;
import com.springcloud.blog.admin.entity.Users;
import com.springcloud.blog.admin.security.entity.SelfUserEntity;
import com.springcloud.blog.admin.service.UsermetaService;
import com.springcloud.blog.admin.service.UsersService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 自定義登錄驗證
 *
 * @Author youcong
 */
@Component
public class UserAuthenticationProvider implements AuthenticationProvider {


    @Autowired
    private UsersService usersService;

    @Autowired
    private UsermetaService usermetaService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 獲取表單輸入中返回的用戶名
        String userName = (String) authentication.getPrincipal();
        // 獲取表單中輸入的密碼
        String password = (String) authentication.getCredentials();
        // 查詢用戶是否存在
         SelfUserEntity userInfo = usersService.getUserInfo(userName);


        if (userInfo.getUsername() == null || userInfo.getUsername() == "") {
            throw new UsernameNotFoundException("用戶名不存在");
        }


        // 我們還要判斷密碼是否正確,這裏我們的密碼使用BCryptPasswordEncoder進行加密的
        if (!new BCryptPasswordEncoder().matches(password, userInfo.getPassword())) {
            throw new BadCredentialsException("密碼不正確");
        }
        // 還可以加一些其他信息的判斷,比如用戶賬號已停用等判斷
        if (userInfo.getStatus().equals("1")) {
            throw new LockedException("該用戶已被凍結");
        }
        // 角色集合
        Set<GrantedAuthority> authorities = new HashSet<>();

        EntityWrapper<Usermeta> roleWrapper = new EntityWrapper<>();
        roleWrapper.eq("user_id",userInfo.getUserId());
        roleWrapper.eq("meta_key","wp_user_level");
        // 查詢用戶角色
        List<Usermeta> sysRoleEntityList = usermetaService.selectList(roleWrapper);
        for (Usermeta sysRoleEntity: sysRoleEntityList){
            authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleEntity.getMetaValue()));
        }
        userInfo.setAuthorities(authorities);
        // 進行登錄
        return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}

7.UserPermissionEvaluator.java

package com.springcloud.blog.admin.security;


import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.springcloud.blog.admin.entity.Usermeta;
import com.springcloud.blog.admin.service.UsermetaService;
import org.apache.catalina.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 自定義權限註解驗證
 * @Author youcong
 */
@Component
public class UserPermissionEvaluator implements PermissionEvaluator {

    @Autowired
    private UsermetaService usermetaService;

    /**
     * hasPermission鑒權方法
     * 這裏僅僅判斷PreAuthorize註解中的權限表達式
     * 實際中可以根據業務需求設計數據庫通過targetUrl和permission做更複雜鑒權
     * 當然targetUrl不一定是URL可以是數據Id還可以是管理員標識等,這裏根據需求自行設計
     * @Author youcong
     * @Param  authentication  用戶身份(在使用hasPermission表達式時Authentication參數默認會自動帶上)
     * @Param  targetUrl  請求路徑
     * @Param  permission 請求路徑權限
     * @Return boolean 是否通過
     */
    @Override
    public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {
        // 獲取用戶信息
        Usermeta selfUserEntity =(Usermeta) authentication.getPrincipal();
        // 查詢用戶權限(這裏可以將權限放入緩存中提升效率)
        Set<String> permissions = new HashSet<>();
        EntityWrapper<Usermeta> roleWrapper = new EntityWrapper<>();
        roleWrapper.eq("user_id",selfUserEntity.getUserId());
        roleWrapper.eq("meta_key","wp_user_level");
        List<Usermeta> sysMenuEntityList = usermetaService.selectList(roleWrapper);
        for (Usermeta sysMenuEntity:sysMenuEntityList) {
            permissions.add(sysMenuEntity.getMetaValue());
        }
        // 權限對比
        if (permissions.contains(permission.toString())){
            return true;
        }
        return true;
    }
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}

五、編寫實體類

package com.springcloud.blog.admin.security.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.Collection;
import java.util.Map;

/**
 * SpringSecurity用戶的實體
 * 注意:這裏必須要實現UserDetails接口
 *
 * @Author youcong
 */
public class SelfUserEntity implements Serializable, UserDetails {

    private static final long serialVersionUID = 1L;

    /**
     * 用戶ID
     */
    private Long userId;
    /**
     * 用戶名
     */
    private String username;
    /**
     * 密碼
     */
    private String password;
    /**
     * 狀態
     */
    private String status;


    /**
     * 显示名稱
     */
    private String displayName;


    /**
     * 用戶參數
     */
    private Map<String, String> userParamMap;


    /**
     * 用戶角色
     */
    private Collection<GrantedAuthority> authorities;
    /**
     * 賬戶是否過期
     */
    private boolean isAccountNonExpired = false;
    /**
     * 賬戶是否被鎖定
     */
    private boolean isAccountNonLocked = false;
    /**
     * 證書是否過期
     */
    private boolean isCredentialsNonExpired = false;
    /**
     * 賬戶是否有效
     */
    private boolean isEnabled = true;


    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }


    public void setAuthorities(Collection<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }


    public void setEnabled(boolean enabled) {
        isEnabled = enabled;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String getStatus() {
        return status;
    }

    public String getDisplayName() {
        return displayName;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    public Map<String, String> getUserParamMap() {
        return userParamMap;
    }

    public void setUserParamMap(Map<String, String> userParamMap) {
        this.userParamMap = userParamMap;
    }

    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return isAccountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return isAccountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return isCredentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return isEnabled;
    }


}

六、編寫JWT接口請求攔截器

package com.springcloud.blog.admin.security.jwt;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.springcloud.blog.admin.config.JWTConfig;
import com.springcloud.blog.admin.security.entity.SelfUserEntity;
import com.springcloud.blog.admin.utils.CollectionUtil;
import com.springcloud.blog.admin.utils.JWTTokenUtil;
import com.springcloud.blog.admin.utils.RedisUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

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;
import java.util.Map;

/**
 * JWT接口請求校驗攔截器
 * 請求接口時會進入這裏驗證Token是否合法和過期
 *
 * @Author youcong
 */
public class JWTAuthenticationTokenFilter extends BasicAuthenticationFilter {

    public JWTAuthenticationTokenFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 獲取請求頭中JWT的Token
        String tokenHeader = request.getHeader(JWTConfig.tokenHeader);

        if (null != tokenHeader && tokenHeader.startsWith(JWTConfig.tokenPrefix)) {
            try {


                // 截取JWT前綴
                String token = tokenHeader.replace(JWTConfig.tokenPrefix, "");
                // 解析JWT
                Claims claims = Jwts.parser()
                        .setSigningKey(JWTConfig.secret)
                        .parseClaimsJws(token)
                        .getBody();
                // 獲取用戶名
                String username = claims.getSubject();
                String userId = claims.getId();


                if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(userId)) {
                    // 獲取角色
                    List<GrantedAuthority> authorities = new ArrayList<>();
                    String authority = claims.get("authorities").toString();
                    if (!StringUtils.isEmpty(authority)) {
                        List<Map<String, String>> authorityMap = JSONObject.parseObject(authority, List.class);
                        for (Map<String, String> role : authorityMap) {
                            if (!StringUtils.isEmpty(role)) {
                                authorities.add(new SimpleGrantedAuthority(role.get("authority")));
                            }
                        }
                    }
                    //組裝參數
                    SelfUserEntity selfUserEntity = new SelfUserEntity();
                    selfUserEntity.setUsername(claims.getSubject());
                    selfUserEntity.setUserId(Long.parseLong(claims.getId()));
                    selfUserEntity.setAuthorities(authorities);
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(selfUserEntity, userId, authorities);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            } catch (ExpiredJwtException e) {
                System.out.println("Token過期");
            } catch (Exception e) {
                System.out.println("Token無效");
            }
        }
        filterChain.doFilter(request, response);
        return;
    }
}

七、SpringSecurity用戶的業務實現

package com.springcloud.blog.admin.security.service;


import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.springcloud.blog.admin.entity.Users;
import com.springcloud.blog.admin.security.entity.SelfUserEntity;
import com.springcloud.blog.admin.service.UsersService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

/**
 * SpringSecurity用戶的業務實現
 *
 * @Author youcong
 */
@Component
public class SelfUserDetailsService implements UserDetailsService {

    @Autowired
    private UsersService usersService;

    /**
     * 查詢用戶信息
     *
     * @Author youcong
     * @Param username  用戶名
     * @Return UserDetails SpringSecurity用戶信息
     */
    @Override
    public SelfUserEntity loadUserByUsername(String username) throws UsernameNotFoundException {

        EntityWrapper<Users> wrapper = new EntityWrapper<>();


        //郵箱正則表達式
        String expr = "^([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})$";

        //是否為郵箱
        if (username.matches(expr)) {
            wrapper.eq("user_email", username);
        } else {
            wrapper.eq("user_login", username);
        }

        // 查詢用戶信息
        Users sysUserEntity = usersService.selectOne(wrapper);
        if (sysUserEntity != null) {
            // 組裝參數
            SelfUserEntity selfUserEntity = new SelfUserEntity();
            BeanUtils.copyProperties(sysUserEntity, selfUserEntity);
            return selfUserEntity;
        }
        return null;
    }
}

八、Spring Security常用註解

1.@Secured

當@EnableGlobalMethodSecurity(securedEnabled=true)的時候,@Secured可以使用。

@PostMapping("/helloUser")
@Secured({"ROLE_normal","ROLE_admin"})
public Map<String, Object> initDashboard() {
        Map<String, Object> result = new HashMap<>();
        result.put(ResponseDict.RESPONSE_TITLE_KEY, "儀錶盤初始化");
        result.put(ResponseDict.RESPONSE_DATA_KEY, dashboardService.initDashboard());
        return ResultUtil.resultSuccess(result);
    }

說明:擁有normal或者admin角色的用戶都可以方法helloUser()方法。另外需要注意的是這裏匹配的字符串需要添加前綴“ROLE_“。

2.@PreAuthorize

Spring的 @PreAuthorize/@PostAuthorize 註解更適合方法級的安全,也支持Spring 表達式語言,提供了基於表達式的訪問控制。

當@EnableGlobalMethodSecurity(prePostEnabled=true)的時候,@PreAuthorize可以使用:

@PostMapping("/initDashboard")
@PreAuthorize("hasRole('100')")
public Map<String, Object> initDashboard() {
    Map<String, Object> result = new HashMap<>();
    result.put(ResponseDict.RESPONSE_TITLE_KEY, "儀錶盤初始化");
    result.put(ResponseDict.RESPONSE_DATA_KEY, dashboardService.initDashboard());
    return ResultUtil.resultSuccess(result);
}

3.@PostAuthorize

@PostAuthorize 註解使用並不多,在方法執行后再進行權限驗證,適合驗證帶有返回值的權限,Spring EL 提供 返回對象能夠在表達式語言中獲取返回的對象returnObject。

當@EnableGlobalMethodSecurity(prePostEnabled=true)的時候,@PostAuthorize可以使用:

@GetMapping("/getUserInfo")
@PostAuthorize(" returnObject!=null &&  returnObject.username == authentication.name")
public User getUserInfo() {
        Object pricipal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        User user;
        if("anonymousUser".equals(pricipal)) {
            user = null;
        }else {
            user = (User) pricipal;
        }
        return user;
}

九、測試

(1)登錄測試,拿到token,如圖:

 

 

(2)請求中如果不攜帶token的話,請求其它接口就會显示沒有登錄的提示,如圖:

 

 

(3)正確的請求應當攜帶token,就像下面這樣,如圖:

 

 

(4)沒有權限請求,如圖:

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

「從零單排canal 02」canal集群版 + admin控制台 最新搭建姿勢(基於1.1.4版本)

canal [kə’næl],譯意為水道/管道/溝渠,主要用途是基於 MySQL 數據庫增量日誌解析,提供增量數據 訂閱 和 消費。應該是阿里雲DTS(Data Transfer Service)的開源版本,開源地址:
https://github.com/alibaba/canal。

canal從1.1.4版本開始引入了admin控制台,有了很多不一樣的配置方式。在搭建過程中如果僅僅按照wiki的用戶手冊,還是容易踩很多坑的。因此,將筆者在搭建過程中的步驟記錄下來,作為官方wiki的 補充,希望能有所幫助。

根據本文內容與搭建順序 ,並搭配對應的官網文檔鏈接,應該就能快速搭建完成了,enjoy~

1. 部署canal-admin

1)部署服務

官方文檔地址:
https://github.com/alibaba/canal/wiki/Canal-Admin-QuickStart

主要配置application.yml文件

server:
  port: 8089

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

spring.datasource:
  address: 127.0.0.1:3306
  database: canal_manager
  username: xxxx
  assword: xxxxx
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false
  hikari:
    maximum-pool-size: 30
    minimum-idle: 1

canal:
  adminUser: admin
  adminPasswd: admin

 

這裏需要注意,canal的adminPasswd並不是登陸admin的密碼,登陸admin的密碼是設置在對應的數據庫中的,默認為123456。

另外,因為 Canal Admin 是一個管理系統,需要使用數據庫存放配置信息,只用在 MySQL 中執行 Canal Admin 提供的數據庫初始化文件即可,該文件在“conf/canal_manager.sql”路徑下面。

2)登陸瀏覽器訪問

上面的 Canal Admin 配置好了之後直接根據“/bin/startup.sh”啟動 Canal Admin 即可,在瀏覽器上面輸入 hostip:8089 即可進入到管理頁面,如果使用的默認的配置信息,用戶名入”admin”,密碼輸入”123456”即可訪問首頁。

進入到首頁點擊集群的菜單欄,然後選擇新建集群。

在裏面輸入集群的名稱以及 Zookeeper即可,這裏的集群目前還沒有任務節點,後續通過配置 Canal Server 的自動註冊功能,便可以查看該集群下面擁有的節點。

創建集群后,需要先配置集群 主配置,載入模板即可。

如果沒有載入這個模版,那麼在canal-server執行 sh bin/startup.sh local 命令,讀取canal_local.propeties配置啟動時,會報錯

1 Caused by: com.alibaba.otter.canal.common.CanalException: requestGet for canal config error: canal.properties config is empty

 

注意,在主配置中,還是需要記得加入zk的地址配置,跟上面配置集群名字的時候輸入的zk無關 (那個zk地址不知道有啥用):

canal.zkServers = xxx.xx.xx.xx:2181,xxx.xx.xx.xx:2181,xxx.xx.xx.xx:2181

 

如果希望使用canal的集群模式(推薦使用),記得更改配置使用default-instance.xml,不要使用file-instance.xml配置,如下:

#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
#canal.instance.global.spring.xml = classpath:spring/file-instance.xml
canal.instance.global.spring.xml = classpath:spring/default-instance.xml

2. 部署canal-server

注意,建議先部署好admin,再來部署canal-server,省事不少。

官方文檔地址:
https://github.com/alibaba/canal/wiki/QuickStart

注意,不同於單機版的properties配置,使用admin后,各種配置通過admin全局管理,所以只需要配置canal_local.properties即可。canal_local.properties內容如下:

# register ip
canal.register.ip =

# canal admin config
canal.admin.manager = 127.0.0.1:8089
canal.admin.port = 11110
canal.admin.user = admin
canal.admin.passwd = xxxxxxxxxxxxxx

# admin auto register
canal.admin.register.auto = true
canal.admin.register.cluster =

 

對各個參數說明一下:

  • canal.register.ip:用來指定當前 Canal Server 的 IP 信息,如果主機是多網卡,可以避免 IP 信息錯亂的問題。
  • canal.admin.passwd:這裏的密碼就是之前配置 Canal Admin 裏面配置的adminPasswd,只不過這裏並不是明文展示,使用 MySQL 的”select password(“admin”)”語句查詢處理過的密碼,注意查詢結果前面的”*”要去掉。
  • canal.admin.register.auto:這裡是自動註冊的意思,如果沒有配置,Canal Server 啟動后需要自行在 Canal Admin 上面添加。
  • canal.admin.register.cluster:這個配置如果不寫代表當前的 Canal Server 是一個單機節點,如果添加的名字在 Canal Admin 上面沒有提前註冊,Canal Server 啟動時會報錯。

啟動server,切記切記帶上參數local,這樣才會讀取canal_local.properties的配置

sh bin/startup.sh local
啟動完成后,可以在admin界面看到server的連接信息

 

同時,登陸zk,查看是否已經註冊成功。

 

3. 配置canal-server

採用admin的集群模式后,集群內的canal-server通過zk做HA,因此,canal-server的配置也只能通過集群做全局配置。

就是前面的集群配置-主配置進入。

 

另外,canal作為一個增量數據抓取模塊,抓到變更信息后需要投遞。

通過canal.sererMode配置

這裏也限制了,一個集群內的canal只能支持一種投遞模式。

我們目前暫時以投遞RocketMQ為例進行配置。

官方文檔地址:
https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart

最終配置文件的配置如下:

#################################################
#########         common argument        #############
#################################################

# tcp bind ip
canal.ip =

# register ip to zookeeper
canal.register.ip =
canal.port = 11111
canal.metrics.pull.port = 11112

# canal instance user/passwd
canal.user = xxxx
canal.passwd = xxxxxxxxxxxxxxxxxxx

# canal admin config
canal.admin.manager = 127.0.0.1:8089
canal.admin.port = 11110
canal.admin.user = xxxx
canal.admin.passwd = xxxxxxxxxxxxxx
canal.zkServers = xx.xx.xx.xx:xxxx,xx.xx.xx.xxx:xxxx,xx.xx.xx.xx:xxxx

# flush data to zk
canal.zookeeper.flush.period = 1000
canal.withoutNetty = false

# tcp, kafka, RocketMQ
canal.serverMode = RocketMQ

# flush meta cursor/parse position to file
canal.file.data.dir = ${canal.conf.dir}
canal.file.flush.period = 1000

## memory store RingBuffer size, should be Math.pow(2,n)
canal.instance.memory.buffer.size = 16384

## memory store RingBuffer used memory unit size , default 1kb
canal.instance.memory.buffer.memunit = 1024

## meory store gets mode used MEMSIZE or ITEMSIZE
canal.instance.memory.batch.mode = MEMSIZE
canal.instance.memory.rawEntry = true

## detecing config
canal.instance.detecting.enable = false

#canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()
canal.instance.detecting.sql = select 1
canal.instance.detecting.interval.time = 3
canal.instance.detecting.retry.threshold = 3
canal.instance.detecting.heartbeatHaEnable = false

# support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery
canal.instance.transaction.size =  1024

# mysql fallback connected to new master should fallback times
canal.instance.fallbackIntervalInSeconds = 60

# network config
canal.instance.network.receiveBufferSize = 16384
canal.instance.network.sendBufferSize = 16384
canal.instance.network.soTimeout = 30

# binlog filter config
# 這裏可以對訂閱的消息做過濾
canal.instance.filter.druid.ddl = true
canal.instance.filter.query.dcl = false
canal.instance.filter.query.dml = false
canal.instance.filter.query.ddl = false
canal.instance.filter.table.error = false
canal.instance.filter.rows = false
canal.instance.filter.transaction.entry = false

# binlog format/image check
canal.instance.binlog.format = ROW,STATEMENT,MIXED
canal.instance.binlog.image = FULL,MINIMAL,NOBLOB

# binlog ddl isolation
canal.instance.get.ddl.isolation = false

# parallel parser config
canal.instance.parser.parallel = true

## concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()
#canal.instance.parser.parallelThreadSize = 16
## disruptor ringbuffer size, must be power of 2
canal.instance.parser.parallelBufferSize = 256

# table meta tsdb info
canal.instance.tsdb.enable = true
canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:}
canal.instance.tsdb.url = jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
canal.instance.tsdb.dbUsername = canal
canal.instance.tsdb.dbPassword = canal

# dump snapshot interval, default 24 hour
canal.instance.tsdb.snapshot.interval = 24

# purge snapshot expire , default 360 hour(15 days)
canal.instance.tsdb.snapshot.expire = 360

# aliyun ak/sk , support rds/mq
canal.aliyun.accessKey =
canal.aliyun.secretKey =

#################################################
#########         destinations        #############
#################################################
canal.destinations =

# conf root dir
canal.conf.dir = ../conf

# auto scan instance dir add/remove and start/stop instance
canal.auto.scan = true
canal.auto.scan.interval = 5
canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml

#canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml
canal.instance.global.mode = manager
canal.instance.global.lazy = false
canal.instance.global.manager.address = ${canal.admin.manager}

#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml

#canal.instance.global.spring.xml = classpath:spring/file-instance.xml
canal.instance.global.spring.xml = classpath:spring/default-instance.xml

##################################################
#########              MQ              #############
##################################################
canal.mq.servers = xx.xx.xx.xx:xxxx,xx.xx.xx.xxx:xxxx,xx.xx.xx.xx:xxxx
canal.mq.retries = 0
canal.mq.canalBatchSize = 50
canal.mq.canalGetTimeout = 100
canal.mq.flatMessage = true
canal.mq.compressionType = none
canal.mq.acks = all

#canal.mq.properties. =
canal.mq.producerGroup = test_canal_cluster

# Set this value to "cloud", if you want open message trace feature in aliyun.
canal.mq.accessChannel = local

# aliyun mq namespace

#canal.mq.namespace =

##################################################
#########     Kafka Kerberos Info    #############
##################################################
canal.mq.kafka.kerberos.enable = false
canal.mq.kafka.kerberos.krb5FilePath = "../conf/kerberos/krb5.conf"
canal.mq.kafka.kerberos.jaasFilePath = "../conf/kerberos/jaas.conf"

 

4. 配置canal instance(以投遞MQ為例)

Canal Admin 提供了 Canal Instance 的管理功能。

我們嘗試通過 UI 界面添加需要監聽的數據庫,讓該 Instance 消費 binlog 並將事件發送到 MQ。

  • 點擊“新建 Instance”按鈕創建 Instance
  • 點擊“載入模板”,進行配置修改。

主要修改以下配置:

  • ”canal.instance.mysql.slaveId”:目前1.1.4版本已經不需要配置,系統自動生成
  • ”canal.instance.master.address”:配置你的數據庫地址
  • canal.instance.dbUsername:數據庫用戶名
  • canal.instance.dbPassword:數據庫密碼
  • canal.mq.topic:mq的topic
  • canal.instance.filter.regex=訂閱的庫表名單(例如:dbvtest\\..*),具體參考https://github.com/alibaba/canal/wiki/AdminGuide

保存,啟動,觀察日誌,沒有報錯即可。

同時可以從 canal-server的 操作-詳情 進入,查看可以看到正在運行的instance

這裏需要注意,如果沒有正確註冊到zk,那麼每個canal-server都會有一個正在運行的instance,會導致變更投遞多次。

如果正確註冊了zk,同一個集群下,每個instance應該只在某一個cannal-server中運行。

可以查看zk路徑

/otter/canal/destinations/{xxxx}/running

 

xxxx就是instance的 名稱,running節點表示它運行在哪個server上。

另外,目前對binlog的訂閱支持gtid模式和position模式,通過以下參數設置

# enable gtid use true/false
canal.instance.gtidon=false

 

在demo過程中,發現高可用模式下,gtid無法正常更新到zk,而position模式使用正常。

gtid模式在canal高可用模式下可能存在bug(參考issue:
https://github.com/alibaba/canal/issues/2616),暫時不建議使用。

5. 演練訂閱

在rds執行以下sql

UPDATE `album` SET `attribute`=’5′ WHERE `albumid`=’1′;

在MQ中的消息格式如下:

{

    "data":[

        {
            "albumid":"1",
            "picid":"1234",
            "attribute":"5",
            "lastmodified":"2020-05-15 18:13:35",
            "created":"2019-09-04 18:18:51"
        }
    ],
    "database":"dbvtest",
    "es":1589537615000,
    "id":75,
    "isDdl":false,
    "mysqlType":{
        "albumid":"int(10) unsigned",
        "picid":"int(10) unsigned",
        "attribute":"varchar(200)",
        "lastmodified":"timestamp",
        "created":"timestamp"
    },
    "old":[
        {
            "attribute":"2",
            "lastmodified":"2020-05-15 17:42:57"
        }
    ],
    "pkNames":[
        "albumid"
    ],
    "sql":"",
    "sqlType":{
        "albumid":4,
        "picid":4,
        "attribute":12,
        "lastmodified":93,
        "created":93
    },
    "table":"album",
    "ts":1589537615877,
    "type":"UPDATE"
}

 

6. 監控告警

官方文檔地址:
https://github.com/alibaba/canal/wiki/Prometheus-QuickStart

如果你已經有一套prometheus的監控體系,我們只需要導入模板(
canal/conf/metrics/Canal_instances_tmpl.json)即可,完美~

 

都看到最後了,原創不易,點個關注,點個贊吧~

知識碎片重新梳理,構建Java知識圖譜: github.com/saigu/JavaK…(歷史文章查閱非常方便)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

年輕人都想要的運動溜背SUV 居然只要5萬多

隔音經不起考驗在低速時,瑞風S2的隔音還是不錯的,但是上到高速以後,風噪、路噪和發動機噪音都會傳到車廂內。總體來講,瑞風S2的隔音較為一般。後排表現有好也有壞瑞風S2的後排腿部空間充裕,但是頭部空間會顯得局促。

前言

瑞風S2作為S3的小弟,雖然銷量上沒有大哥那麼火爆,但是綜合實力同樣不可小覷。實際表現到底如何,下面編者我就給大家好好說一下。

江淮汽車-瑞風S2

指導價:5.88-7.68萬

外觀光鮮亮麗

瑞風S2的車身尺寸為4135*1750*1568mm,軸距為2490mm。得益於車身較寬,整個前臉看上去很飽滿有力,中間幾條鍍鉻裝飾條把車頭點綴得很閃亮。

車側的設計有一點點怪異,主要是門把手的設計不在一條直線上而是呈一定角度向後揚,輪轂的樣式則很像一股旋風。

尾門的設計不夠大氣,同時導致地台的開起角度偏高。兩大燈之間用了凸起的線條來連接顯得突兀,隱藏式的排氣讓尾部下方很乾凈。

內飾平平無奇

一眼看去,瑞風S2的中控台給人滿滿的塑料感,紅色與黑色的搭配使用並沒有帶來一種亮麗的效果,反而更顯廉價。車門上的銀色護板設計算是內飾中為數不多的亮點。

動力數據優於同級對手

瑞風S2隻有一副1.5L自然吸氣發動機,最大輸出113馬力和146牛米。這個數據無論是哈弗H1還是長安CS15,都無法與之匹敵。與這副發動機匹配的是5擋手動變速箱和CVT變速箱,整體的動力表現一般,因為這個級別的SUV多是搭載1.5L發動機,導致先天動力不夠充足,而瑞風S2的車身更為龐大,所以在高轉時的發力不太好,在2000到3000轉是車子的最佳動力區間。

底盤表現中規中矩

毫無意外地,瑞風S2的后懸架也是使用扭力梁式非獨立懸架。在面對震動時的過濾表現得不錯,但是當路面較為複雜,需要去連續吸收和回彈時,瑞風S2總是餘震不斷,不夠利索。

隔音經不起考驗

在低速時,瑞風S2的隔音還是不錯的,但是上到高速以後,風噪、路噪和發動機噪音都會傳到車廂內。總體來講,瑞風S2的隔音較為一般。

後排表現有好也有壞

瑞風S2的後排腿部空間充裕,但是頭部空間會顯得局促。中央的凸起不高,座椅雖然寬大,但是包裹性不強。

油耗與保養費用

多位車主反映瑞風S2手動擋車型的百公里綜合油耗為8.1L,而自動擋車型為8.9L。6萬公里的總保養費用上,手動擋車型為4639元,而自動擋車型為4585元。

油耗上的表現偏高,而保養費用則在合理範圍內。

配置分析

瑞風S2的2017款車型全系標配ESp,這點很厚道。

手動擋車型我會推薦買指導價為5.88萬的17款手動舒適型,即最低配。因為這個配置已經包含了一些實用配置,不過沒有倒車雷達,這個可以自己後期加裝,沒必要為此加多4000元上手動豪華型。

自動擋車型我會建議買指導價為7.08萬的15款CVT豪華型,即自動擋的最低配。雖然是最低配,但也已經配有了主副駕駛座安全氣囊、中控彩色大屏、ESp和倒車雷達等實用配置。需要注意的是,15款車型的車標還是五角星而非英文字母。

編者總結

瑞風S2價格較低,不過即將迎來寶駿510的挑戰,恐怕也會有些難以抵擋。但是,作為這個價格區間中,少有的帶自動擋的車型,相信瑞風S2還是可以俘獲不少消費者的心。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計最專業,超強功能平台可客製化

首份全球報告:1970年以來 洄游性淡水魚族群下降76%

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

疫情肆虐導致經濟停擺 美國能源消耗創新低

摘錄自2020年7月30日中央社報導

美國能源情報署(Energy Information Administration)官員今天(29日)表示,美國經濟由於武漢肺炎(COVID-19)疫情而停擺,發電用燃煤與用來提煉汽油和航空燃料的原油需求減少,今(2020年)春能源消耗銳減至30多年來新低水準。

除了美國外,全球各地能源使用也因疫情影響而一致偏低,當商業活動恢復後,這樣的趨勢可望翻轉,但預計美國與全球的年度溫室氣體排放量仍將減少。

美國能源情報署說,相較於去(2019)年同期,4月份全美能源消耗減少14%,這是自1989年以來的單月新低,並且是自1973年有資料彙整以來創下的最大降幅。預估今年下半年能源總消耗量將增加,不過仍低於去年的水準。

氣候變遷
溫室氣體
能源轉型
能源議題
國際新聞
美國
燃煤發電
疫情看氣候與能源
溫室氣體排放

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

※網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

射殺烏干達「明星」大猩猩拉飛奇 盜獵者判刑11年

摘錄自2020年7月30日自由時報報導

烏干達著名的「明星」大猩猩拉飛奇(Rafiki),6月遭盜獵者拜亞穆卡瑪(Felix Byamukama)刺殺,經當局深入調查,拜亞穆卡瑪認罪並坦承自己還曾獵殺其他野生動物,今被法院判處11年徒刑。

雖然拜亞穆卡瑪認罪,但其他3人否認相關指控,目前仍被羈押等待審判結果。山地大猩猩是瀕臨絕種的珍稀動物,全世界僅有約1000多隻。原本拜亞穆卡瑪被認為可能會面臨無期徒刑,但最後被判入獄11年,西澳大學一位發言人表示,這是因為拜亞穆卡瑪不是在特別的野生動物法庭受審。

生物多樣性
生態保育
物種保育
國際新聞
烏干達
銀背大猩猩

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

穿著制服的盜伐集團 官商聯手摧毀柬埔寨保護區林地

環境資訊中心綜合外電;黃鈺婷 翻譯;林大利 審校;稿源:Mongabay

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

還在看奔馳寶馬?這台車要成為豪華轎車新標杆了

在奔馳還沒有改款的時候,在寶馬還在強調操控的時候,奧迪無論在B級或C級市場都毋容置疑是神一樣的存在,在家用及商務用車兩大領域均獨佔鰲頭,但隨着某些政策的改變、奔馳的改款、寶馬的革新,競爭變得激烈起來,對於奧迪而言,產品的更新及新技術的研發已是迫切需要實行的戰略。

9月10號,全新奧迪A4L 正式上市,售價為29.98萬-41.28萬,歷經四十餘載,9代車型更迭,我們非常榮幸的等到了這台融合了現時奧迪最新科技的結晶成果、代號為B9的全新一代奧迪A4L。基於奧迪全新的MLB EVO(縱置發動機模塊化)平台打造,全新的家族外觀、全新的動力總成使得全新一代A4L有了捍衛自家地位的實力,自此,全新奧迪A4L正式接過B8的衣缽,繼續向豪華中型車標杆這一目標邁進。

在國人的印象中,A4L和3系相比可能缺少了純粹原始的運動味道,與奔馳C級相比,似乎又不及它自上而下傳承的雍容豪華,但難能可貴的是,A4L將兩者間的平衡做到了極致,在運動與雍容間也找到了最有自己特性的平衡點,以嚴謹的工匠精神,打造出在當時看幾乎是無可挑剔的科技與机械的結晶,再加上長軸距的幫助,上一代A4L的脫穎而出也就是順理成章的事情了。A4作為奧迪全球銷量最高的車型,在中國市場上為奧迪品牌立下了汗馬功勞,它給奧迪帶來了太多的榮耀和戰績,而A4L老款車型8年累計銷量超過70萬的輝煌戰績、在豪華B級車市場地位穩坐頭把交椅的實力,放眼整個豪華B級車市場,絕對無人能及。從奧迪A4L還沒進入中國市場之前,就已拿到德國“金方向盤”權威大獎。

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計最專業,超強功能平台可客製化

這台10萬內1.5T的自主SUV 不僅動力猛還省油!

油耗表現怎樣。1。5T自動擋車型的車主口碑油耗:9。3L/100km1。5T手動擋車型的車主口碑油耗:8。5L/100km油耗方面,因為變速箱的反應积極也採用了小排量渦輪增壓發動機,所以油耗表現也屬於中等水平。有趣的是採用1。5L自然吸氣的車型油耗和1。

自主小型SUV市場中風起雲涌,中華V3在這一細分市場中銷量一直表現穩定,在今年的前11個月裏面,它的總銷量達94937輛!

這樣的銷量表現不錯,下面我們就來聊聊這款SUV的駕駛體驗到底怎樣。

華晨中華-V3

指導價:6.57-11.77萬

動力總成:

中華V3採用1.5L或1.5T兩款發動機,而編者試駕的是採用1.5T的車型,這款發動機的最大功率150馬力,峰值扭矩220牛米/2000-4500轉。

變速箱方面採用5擋手自一體、5擋手動變速箱,而編者體驗的正是採用1.5T配合5擋手自一體的自動擋車型。

駕駛體驗如何:

這款發動機的功率不低,只是峰值扭矩不算太早到來,所以起步的動力沒有太強勁的表現,油門反應很線性、雖然低轉速扭矩一般,但是當轉速突破了2000轉以後,動力會迎來一個爆發!

2000轉確實是一個分水嶺,這時候深踩油門,推背感變得很明顯。

它的5擋手自一體變速箱給我帶來一些驚喜,其換擋邏輯、換擋速度都比較優異!還有平順度也有不錯的表現。

在急加速的時候,它會直接從5擋降到2擋或3擋!加速的表現相當給力。

只是前進擋位只有5個,所以在高速巡航時轉速稍微有一點點高,就像120km/h巡航時,發動機轉速為2300轉左右。

中華V3的1.5T自動擋車型的百公里加速時間約為10.44秒,這樣的加速成績是比較不錯的。

油耗表現怎樣?

1.5T自動擋車型的車主口碑油耗:9.3L/100km

1.5T手動擋車型的車主口碑油耗:8.5L/100km

油耗方面,因為變速箱的反應积極也採用了小排量渦輪增壓發動機,所以油耗表現也屬於中等水平。有趣的是採用1.5L自然吸氣的車型油耗和1.5T的比較接近。

競爭對手分析:

江淮汽車-瑞風S3

指導價:6.58-9.58萬

瑞風S3的動力沒有中華V3強,但由於採用了CVT變速箱的緣故,所以油耗比較低。自動擋車型的車主口碑油耗約為7.9L/100km。

長城汽車-哈弗H2s

指導價:8.38-10.28萬

中華V3的1.5T車型與哈弗H2s直接競爭!這兩款車的尺寸非常相近,而同價位車型對比的話,哈弗H2s的配置更加豐富、而且H2s採用了7擋濕式雙離合變速箱,在油耗方面有一定優勢。

但是,中華V3的起售價比較低,車型選擇豐富,對於喜歡性價比的買家來說,依然是個不錯的選擇。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

聽說這台12萬起的合資轎車代表了德系汽車精髓?

空間表現高爾夫的後座空間表現良好,座墊長度足夠,能很好地承托住大腿。不過,中央隆起很明顯,嚴重影響後座中間乘客的舒適性。油耗與保養費用多位車主反映高爾夫1。4T手動版的百公里綜合油耗為6。9L,而自動版而7。2L。

前言

說起德系的兩廂車,不得不提的就是高爾夫。雖然高爾夫現在銷量一般,但是卻蘊含了很多大眾的核心技術在裏面。下面編者我就給大家好好說這款運動型兩廂車。

一汽-大眾-高爾夫

指導價:12.19-23.99萬

外觀簡約時尚

高爾夫的車身尺寸為4255*1799*1452mm,軸距為2637mm。整個前臉也是以簡約為主,沒有太多的裝飾。車身側面也只有一條平直的腰線,尾部設計同樣是很簡單,沒太多可說的。

內飾是經典的大眾風

高爾夫的中控略微向駕駛員傾斜,整個色調以黑色和銀色為主。按鈕的布局簡潔整齊,上手難度不大。

動力組合眼花繚亂

高爾夫一共有六套動力組合,分別是1.2T+7擋雙離合、1.4T+5擋手動或7擋雙離合、1.6L+5擋手動或6擋手自一體,以及2.0T+7擋雙離合。

這次編者我就着重講一下高爾夫賣得最好的黃金動力組合——1.4TSI+7擋DSG。1.4T發動機最大輸出131馬力和225牛米,最大扭矩從1500轉爆發直到3500轉,這是低功率版本的數據。1.4T還有一個高功率版本,這裏就不展開說明。

在市區裏面開,1.4T的高爾夫動力很充沛,低速蠕動時也沒有很明顯的頓挫,深踩油門時的降擋乾脆利索,平順得不像是一台雙離合變速箱。

底盤表現已是儘力之作

高爾夫的后懸架配的是扭力梁式非獨立懸架,這個因素導致它在過一些連續顛簸路面時的貼服感一般,同時無法對路面的信息進行快速處理,濾震效果一般。在過一些連續彎路時,底盤的極限又很高,側傾抑製得很不錯。

空間表現

高爾夫的後座空間表現良好,座墊長度足夠,能很好地承托住大腿。不過,中央隆起很明顯,嚴重影響後座中間乘客的舒適性。

油耗與保養費用

多位車主反映高爾夫1.4T手動版的百公里綜合油耗為6.9L,而自動版而7.2L。1.6L手動版車型為7.1L,而自動版為8L。

按照廠家建議,1.4T手動版六萬公里的總保養費用為3503元,而自動版為3257元。

安全配置突出

在眼花繚亂的配置表中,我會建議買指導價為15.49萬的1.4T雙離合版的最低配車型。這個車型已經包含了很多重要配置,例如前後排頭部氣囊、胎壓監測、ESp、后駐車雷達、中控大屏等等。

競爭對手

長安福特-福克斯

指導價:9.98-16.58萬

編者點評:這兩款車都是運動取向的車,在價格相近的情況下,福克斯的發動機動力輸出更猛,車身尺寸也要明顯大於高爾夫。同時福克斯喝92號汽油,而高爾夫要喝95號汽油。福克斯配有後輪獨立懸架,在濾震和操控性表現上要更好。不過,配置上,高爾夫會明顯佔優,在安全裝備和燈光配置上有較大優勢。

編者總結:

作為MQB平台的首輛國產車,可以看出大眾對高爾夫寄予厚望。從實際產品力來說,高爾夫也確實沒有辜負大家對它的期待。不過,12.19萬的起步價還是偏高了一點。如果價格可以像福克斯那樣下探到10萬內,銷量突入前十是一件指日可待的事。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!