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…(歷史文章查閱非常方便)

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

11萬就能買到舒適性堪比天籟的SUV 你覺得值不值

T90最大的賣點來了,都說自主品牌的動力系統調教的不好,T90直接使用了日產MR20的2。0L自然吸氣發動機,最大功率144馬力,最大扭矩198牛·米,匹配CVT變速箱(低配為6擋手動)。這套動力系統目前也用在天籟和逍客上面搭載,看到這些,你還會擔心T90的動力系統么。

12月25日晚上,東風日產啟辰中型SUV—T90正式上市,價格區間為10.98-15.48萬元。

T90也是小編比較期待的一款自主中型SUV,因為此車有着時尚的外觀,精緻的內飾,舒適的座椅,同時和日產有着千絲萬縷的聯繫,所以小編一直很關注它,直到看到它的正式售價,心裏的石頭也終於落地了,畢竟在價格上,啟辰沒有“逗我們玩”。

T90定位中型轎跑型SUV,目前是啟辰的旗艦車型。T90的車身尺寸為4793*1865*1592mm(不同車型高度略有差異),軸距為2765mm,身材在同級別車型處於正常水準。T90的外觀和寶馬X6、歌詩圖有點類似,都是有着溜背的造型,再加上車子較長,所以T90的側面看起來非常的修長,惹人喜愛。

T90的設計理念為“風雕美學”,這是啟辰第一次運用這個理念設計出來的車型,所以T90的整體外觀看起來會使人眼前一亮,同時別緻的前進氣格柵也為T90加分不少。

內飾也是很討人喜歡的地方,中控台都採用了軟性材質覆蓋,摸起來很柔軟,手感很好,同時做工也比較精細,看得出裝配工藝還是很嚴格的。中控台採用了分辨率高達1920*768的12.3英寸的大屏幕,不僅大氣,同時畫面也比較清晰,運行很流暢,客戶體驗效果很好。

座椅也是T90的亮點,因為T90的尺寸足夠大,所以它的內部空間還是非常寬敞的,尤其是T90採用了和天籟同級別的人體工程學座椅,而移動大沙發是天籟的綽號,所以T90座椅的舒適程度可想而知了。不過遺憾的就是溜背的造型會犧牲一定的後排頭部空間,那些大個子乘客可能要盡量坐到前排了。

T90最大的賣點來了,都說自主品牌的動力系統調教的不好,T90直接使用了日產MR20的2.0L自然吸氣發動機,最大功率144馬力,最大扭矩198牛·米,匹配CVT變速箱(低配為6擋手動)。這套動力系統目前也用在天籟和逍客上面搭載,看到這些,你還會擔心T90的動力系統么?

說了這麼多,大家一定會說T90哪款車的性價比最高了。除了低配和頂配兩款車,T90的同一級別車型的自動和手動車型差價一萬元,相當於多花10000元買一台CVT變速箱,這個價格在同級別屬於正常水平。

其中最低配手動辰尚版配備了主/副駕氣囊、無鑰匙啟動/進入、剎車輔助、多功能方向盤、中控大屏、藍牙電話、後座出風口,配置比較實用,基本可以滿足日常行車需求。唯一遺憾的就是沒有ESp。

手動風尚版和手動辰尚版差價8000元,但是只是多了這兩個配置,性價比較低。而CVT風尚版比手動風尚版貴10000元,多了CVT變速箱,這也可以得出CVT風尚版的性價比也是比較低的。

智尚版僅僅比風尚版貴8000元,但是卻多了這麼多的配置,不得不說性價比真的很高。

頂配CVT領尚版比次頂配CVT智尚版貴1.9萬元,但是只是多了后駐車雷達、腰部支撐調節、前排座椅加熱、LED近光燈、車內氛圍燈、併線輔助、全景攝像頭和更大的輪轂,總體來看性價比並不高。

如果資金不充足,最低配手動辰尚版性價比非常高,如果想要一些配置,那麼智尚版的性價比非常高。總體來說啟辰的質量口碑很好,在加上T90和日產的關係,所以T90也是一款不錯的車型,不過受制於啟辰的知名度限制,能不能熱銷,還是要看T90的造化了。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

28歲能買這三款車 最能彰顯你與眾不同!

值得一提的是2。0T車型的百公里加速成績在6。93秒左右。在同級中表現優秀。編者點評:速派外觀設計自成一格,而且動力和油耗表現不錯,1。4T自動擋車型的車主口碑油耗在7。3左右,而1。8T車型則為9。5L。它的底盤調教側重於舒適性,濾振效果也不錯。

中型車在過去被賦予了太多的商務定義

所以設計一向都以大氣、沉穩為主

因為老闆們做生意

總是要求座駕夠穩重

這樣做生意能有許多便利

(圖為大型車 大眾輝騰)

而現在中型車已不再完全向商務性傾斜

因為買家的年齡逐漸下降!

那麼今天我們就來聊聊28歲到30歲之間買什麼樣的合資中型車,最能體現年輕人的個性、與眾不同!

一汽馬自達-阿特茲

指導價:17.58-23.98萬

阿特茲的外觀設計盡顯東方設計之靈性,在日系中型車中是最有個性的!它極具動感的外觀、內飾設計讓許多年輕消費者為之買單。

它採用了帶創馳藍天技術的動力總成,2.0L和2.5L發動機均採用直噴的供油方式,還帶有雙可變氣門正時控制系統!

而車身長寬高為4870*1840*1450mm,軸距達2830mm。車身尺寸屬於中等水平,而它的乘坐空間其實也充足夠用。

編者點評:

阿特茲的運動底子好,這是它的一大優勢,在轉向精準性、動力響應性、底盤運動性方面都有不錯的表現!而且車主反饋它的平均百公里油耗在8.1-8.9L左右(含2.0L和2.5L車型),這麼低的油耗是一系列創馳藍天技術協調運作的結果!

上汽大眾斯柯達-速派

指導價:16.98-27.68萬

鋒利、前衛的外觀,讓速派吸引了很多年輕、熱愛個性的消費者關注!它的車身長寬高為4861*1865*1489mm,軸距達2841mm。

動力系統方面,速派採用了1.4T、1.8T、2.0T三款渦輪增壓發動機,搭配5擋手動、7擋雙離合變速箱。值得一提的是2.0T車型的百公里加速成績在6.93秒左右!在同級中表現優秀。

編者點評:

速派外觀設計自成一格,而且動力和油耗表現不錯,1.4T自動擋車型的車主口碑油耗在7.3左右,而1.8T車型則為9.5L。它的底盤調教側重於舒適性,濾振效果也不錯。

廣汽本田-雅閣

指導價:16.98-27.98萬

雅閣在改款之後,走起了前衛、運動路線,這樣的設計讓它的銷量一下子得到了提升!它的車身尺寸為:4915*1845*1470mm,軸距為2775mm。雖然軸距比起阿特茲和速派不佔優勢,但是雅閣的車身長度卻是最長的,實際的空間表現也不差。

雅閣搭載2.0L、2.4L兩款發動機,搭配CVT變速箱 ,而且還推出了混合動力車型。

編者點評:

雅閣擁有炫麗的外觀、充足的空間,而且對比起凱美瑞、天籟這兩款車,雅閣更具有運動性。它的2.4L車型百公里加速成績在8.68秒左右,口碑油耗則為9.1L,在這兩方面表現都比較良好。

最後總結:

這三款車當中,操控性最好的車型當屬阿特茲,而且它也擁有着更加運動化的外觀。而雅閣和速派的舒適性表現則更好。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

※台北網頁設計公司全省服務真心推薦

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

※推薦評價好的iphone維修中心

小號的“本田思域”?7.98萬起售的它實力不容小覷

相對於大燈外形的變化,光源的變化是最讓我們吃驚的,因為這次鋒范使用的將是集中在大燈中的LED日間行車燈以及LED大燈,脫離了“蠟燭燈”的稱號,並且該價位該級別唯一使用LED大燈的車型。不過尾燈方面基本保留着原有的造型,並且根據細節可以推斷鋒范依然會使用鹵素燈源,這點稍顯遺憾,若是能使用LED燈束式設計辨識度以及顏值肯定上升不少。

前言

本田在這一年內發力非常猛烈,一輛本田思域以及1.5T發動機就引起了不少人的關注。思域個性的外觀更是讓人慾罷不能,但思域以下的車型多半都是以着中庸為主的,其中就算本田鋒范最為明顯,平淡無奇的外觀看着白開水一般,但卻適合絕大多數的審美。本田不滿於此,給我們帶來小改款的本田鋒范。

從這次的官圖來看,本田鋒范將基本保持原來的造型的,但是保險杠進行了調整,有着稍微突出的前吻,而且大燈造型也進行了改變。

前臉將使用的是家族式設計,黑色蜂巢狀中網並且有着一條延伸到大燈上部的鍍鉻裝飾條。大燈樣式也變得和思域類似,就像是思域大燈的縮小版。

相對於大燈外形的變化,光源的變化是最讓我們吃驚的,因為這次鋒范使用的將是集中在大燈中的LED日間行車燈以及LED大燈,脫離了“蠟燭燈”的稱號,並且該價位該級別唯一使用LED大燈的車型。

不過尾燈方面基本保留着原有的造型,並且根據細節可以推斷鋒范依然會使用鹵素燈源,這點稍顯遺憾,若是能使用LED燈束式設計辨識度以及顏值肯定上升不少。

(上圖為現款本田鋒范)

新款本田鋒范將在2017年1月12日在海外市場上市,相信我國市場進行改款也只是時間的問題,本田能把LED大燈逐漸下放到經濟型轎車看得出LED大燈成本在迅速下降。

競爭對手依然是大眾捷達、桑塔納、日產陽光這些合資對手,當然還有着奇瑞艾瑞澤5、長安悅翔V7這些國產對手,若是這次改款鋒范能儘早進入我國相信能給它們造成不少的打擊。畢竟鋒范原本的油耗表現就相當喜人,而如今則是剛剛換上油耗更低的1.5L發動機的新捷達能與之一戰。

不知道各位網友對這改款后的鋒范又是怎樣的看法呢?是否會因為這個新外觀以及LED大燈所埋單?本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

可能不到10萬就買到1.5T自動7座SUV,你還要考慮銳界?

內飾設計感更強中控台採用了對稱式的布局,中控屏周圍加以黑色鋼琴烤漆面板點綴,鍍鉻飾條包邊,整體的設計感很強,8英寸的中控大屏是比較搶眼的配置,除了一般多媒體功能外還支持導航和Carlife手機映射等功能,觸控的空調按鈕操作方便更顯檔次感,而且還會新增發動機啟停,胎壓監測、行車記錄儀等配置,省去了後面加裝的麻煩。

前言

作為長安首款7座SUV,CX70上市已經有大半年的時間了,實惠的價格能買到大7座SUV,寬敞的空間布局也比較符合現在家庭用戶的需求,但是其比較單一的1.6L+5MT的動力總成,對那些不會開手動擋或者對動力有追求的消費者來說,就比較糾結了,最終1.5T+6AT的解決方案出來了,能讓你滿意嗎?

外觀

更加霸氣

前臉的進氣格柵變化是比較明顯的,大小不一的鍍鉻飾塊錯落有致的分佈在中網上,看上去精緻了許多,門把手和側護板上增加了鍍鉻裝飾,提升了不少檔次感,外擴的后保險杠造型讓車身長度增加了25mm,並且覆蓋了金屬裝飾板,既起到保護作用,又使硬朗的造型中透露着細膩感。

內飾

設計感更強

中控台採用了對稱式的布局,中控屏周圍加以黑色鋼琴烤漆面板點綴,鍍鉻飾條包邊,整體的設計感很強,8英寸的中控大屏是比較搶眼的配置,除了一般多媒體功能外還支持導航和Carlife手機映射等功能,觸控的空調按鈕操作方便更顯檔次感,而且還會新增發動機啟停,胎壓監測、行車記錄儀等配置,省去了後面加裝的麻煩。

空間

一如既往的大

2780mm的軸距,加上調整后的保險杠樣式使其車身增加了25mm至4705mm,在同級別競爭對手來說有一定的優勢,採用2+3+2的座椅布局,雙色拼接的座椅設計視覺效果出色。

動力

新的組合

動力方面新增了一款1.5T發動機,最大功率為150馬力,在同排量車型中來說表現中規中矩,與之匹配的是6擋手動,或者是來自愛信的6擋手自一體變速箱,想購買該款車型的消費者終於等來了渦輪增壓車型。

總結:從產品力來看,長安CX70終於補齊了沒有自動擋車型的短板,在逐漸激烈的7座SUV市場里提升了不少的戰鬥力,但具體性能表現如何,還得上市后才見分曉,還有價格上小編推測應該是很有吸引力的一個價格。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

年輕人都想要的運動溜背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地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

蘋果碳中和實驗:拆解舊iPhone、回收稀土的機器人

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

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

※台北網頁設計公司全省服務真心推薦

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

※推薦評價好的iphone維修中心

中國大規模漁船隊現厄瓜多「進化島」海域 政府憂威脅脆弱生態

摘錄自2020年7月29日明報報導

據報,厄瓜多海軍近日發現一支由約260艘中國漁船組成的船隊,在所屬加拉帕戈斯群島專屬經濟海域的公海海域一帶捕魚。透過監測,發現該船隊包括漁船、補給船和冷凍船,跟一般遠洋漁船隊相若,規模卻是近年所見最大。引起當地政府大為緊張,恐對該世界自然遺產脆弱的生態系統構成威脅,正予警戒監視,並計畫循外交途徑促船隊離開。

加拉帕戈斯群島離厄瓜多海岸563英里,擁有富饒多元的生態,包括瀕臨滅絕的鯨魚、鎚頭鯊等稀有物種,是全球鯊魚種類最豐富的地區之一,亦是海龜、火烈鳥和信天翁的棲地,1978年列入聯合國教科文組織世界自然遺產。英國科學家達爾文的「演化論」是受該群島啟發,也因此被稱為「進化島」。

首都基多(Quito)前市長塞維利亞指出,中國漁船在保護區邊緣活動「正摧毁厄瓜多爾致力保護加拉帕戈斯群島海洋生物的努力」。

生物多樣性
海洋
國際新聞
厄瓜多
進化島
漁船
世界自然遺產
生態脆弱
保護區

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

【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

非洲南部面臨糧食危機 230萬兒童生命受威脅

摘錄自2020年7月29日中央社約翰尼斯堡報導

受到乾旱、洪水和武漢肺炎疫情的影響,非洲南部13個國家約有4500萬人存在糧食危機。28日非南開發共同體(Southern African Development Community,SADC)發表的報告表示這個統計數字較去年增加近10%。

報告指出,經常出現的氣候異常所引發的挑戰,加上2019冠狀病毒疾病(COVID-19)所造成的影響,加劇經濟衰退與人民貧困。尤其是重度依賴非常態性工作以及依靠當地市場維生的人。2020年非洲南部國家中約有840萬的兒童將面臨嚴重營養不良,其中230萬兒童的生命將受到威脅。預測糧食危機的情況會進一步惡化。

非南開發共同體成員國中,南非是受到疫情衝擊最嚴重的國家。截至7月28日為止,南非確診病例累計達45萬9761例,7257人死亡,確診患者中有28萬7313人已康復出院。

土地利用
氣候變遷
國際新聞
非洲
南非
糧食安全
糧食危機
武漢肺炎
疫情下的社會衝突
災害
糧食
貧窮

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

【其他文章推薦】

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

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

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

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

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

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