01-整合Spring Security
一、Spring Security介紹
1、框架介紹
Spring 是一個非常流行和成功的 Java 應用開發框架。Spring Security 基於 Spring 框架,提供了一套 Web 應用安全性的完整解決方案。一般來說,Web 應用的安全性包括 用戶認證 (Authentication)和用戶授權( Authorization)兩個部分。
(1)用戶認證指的是:驗證某個用戶是否爲系統中的合法主體,也就是說用戶能否訪問該系統。用戶認證一般要求用戶提供用戶名和密碼。系統通過校驗用戶名和密碼來完成認證過程。
(2)用戶授權指的是驗證某個用戶是否有權限執行某個操作。在一個系統中,不同用戶所具有的權限是不同的。比如對一個文件來說,有的用戶只能進行讀取,而有的用戶可以進行修改。一般來說,系統會爲不同的用戶分配不同的角色,而每個角色則對應一系列的權限。
Spring Security其實就是用filter,多請求的路徑進行過濾。
(1)如果是基於Session,那麼Spring-security會對cookie裏的sessionid進行解析,找到服務器存儲的sesion信息,然後判斷當前用戶是否符合請求的要求。
(2)如果是token,則是解析出token,然後將當前請求加入到Spring-security管理的權限信息中去
2、認證與授權實現思路
如果系統的模塊衆多,每個模塊都需要就行授權與認證,所以我們選擇基於token的形式進行授權與認證,用戶根據用戶名密碼認證成功,然後獲取當前用戶角色的一系列權限值,並以用戶名爲key,權限列表爲value的形式存入redis緩存中,根據用戶名相關信息生成token返回,瀏覽器將token記錄到cookie中,每次調用api接口都默認將token攜帶到header請求頭中,Spring-security解析header頭獲取token信息,解析token獲取當前用戶名,根據用戶名就可以從redis中獲取權限列表,這樣Spring-security就能夠判斷當前請求是否有權限訪問
二、整合Spring Security
1、在common下創建spring_security模塊
2、在spring_security引入相關依賴
<dependencies>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>common_utils</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Spring Security依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>
*3、在service_acl引入**spring_security**依賴**
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>spring_security</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
0、代碼結構說明:
4、創建spring security核心配置類**
Spring Security的核心配置就是繼承WebSecurityConfigurerAdapter並註解@EnableWebSecurity的配置。
這個配置指明瞭用戶名密碼的處理方式、請求路徑的開合、登錄登出控制等和安全相關的配置
import com.atguigu.serurity.filter.TokenAuthenticationFilter;
import com.atguigu.serurity.filter.TokenLoginFilter;
import com.atguigu.serurity.security.DefaultPasswordEncoder;
import com.atguigu.serurity.security.TokenLogoutHandler;
import com.atguigu.serurity.security.TokenManager;
import com.atguigu.serurity.security.UnauthorizedEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* <p>
* Security配置類
* </p>
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
private TokenManager tokenManager;
private DefaultPasswordEncoder defaultPasswordEncoder;
private RedisTemplate redisTemplate;
@Autowired
public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
TokenManager tokenManager, RedisTemplate redisTemplate) {
this.userDetailsService = userDetailsService;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
/**
* 配置設置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnauthorizedEntryPoint())
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and().logout().logoutUrl("/admin/acl/index/logout")
.addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()
.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
.addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();
}
/**
* 密碼處理
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}
/**
* 配置哪些請求不攔截
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**",
"/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"
);
}
}
5、創建認證授權相關的工具類
(1)DefaultPasswordEncoder:密碼處理的方法
package com.atguigu.serurity.security;
import com.atguigu.commonutils.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* <p>
* 密碼的處理方法類型
* </p>
*/
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
public DefaultPasswordEncoder() {
this(-1);
}
/**
* @param strength
* the log rounds to use, between 4 and 31
*/
public DefaultPasswordEncoder(int strength) {
}
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
(2)TokenManager:token操作的工具類
import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* <p>
* token管理
* </p>
*/
@Component
public class TokenManager {
private long tokenExpiration = 24*60*60*1000;
private String tokenSignKey = "123456";
public String createToken(String username) {
String token = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
return token;
}
public String getUserFromToken(String token) {
String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
return user;
}
public void removeToken(String token) {
//jwttoken無需刪除,客戶端扔掉即可。
}
}
(3)`TokenLogoutHandler:退出實現
import com.atguigu.commonutils.R;
import com.atguigu.commonutils.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>
* 登出業務邏輯類
* </p>
*/
public class TokenLogoutHandler implements LogoutHandler {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = request.getHeader("token");
if (token != null) {
tokenManager.removeToken(token);
//清空當前用戶緩存中的權限數據
String userName = tokenManager.getUserFromToken(token);
redisTemplate.delete(userName);
}
ResponseUtil.out(response, R.ok());
}
}
(4)UnauthorizedEntryPoint:未授權統一處理
import com.atguigu.commonutils.R;
import com.atguigu.commonutils.utils.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* <p>
* 未授權的統一處理方式
* </p>
*/
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
6、創建認證授權實體類
(1)SecutityUser
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* <p>
* 安全認證用戶詳情信息
* </p>
*/
@Data
@Slf4j
public class SecurityUser implements UserDetails {
//當前登錄用戶
private transient User currentUserInfo;
//當前權限
private List<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
(2)User
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* <p>
* 用戶實體類
* </p>
*/
@Data
@ApiModel(description = "用戶實體類")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "微信openid")
private String username;
@ApiModelProperty(value = "密碼")
private String password;
@ApiModelProperty(value = "暱稱")
private String nickName;
@ApiModelProperty(value = "用戶頭像")
private String salt;
@ApiModelProperty(value = "用戶簽名")
private String token;
}
7、創建認證和授權的filter
(1)TokenLoginFilter:認證的filter
import com.atguigu.commonutils.R;
import com.atguigu.commonutils.utils.ResponseUtil;
import com.atguigu.serurity.entity.SecurityUser;
import com.atguigu.serurity.entity.User;
import com.atguigu.serurity.security.TokenManager;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
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;
/**
* <p>
* 登錄過濾器,繼承UsernamePasswordAuthenticationFilter,對用戶名密碼進行登錄校驗
* </p>
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 登錄成功
* @param req
* @param res
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
SecurityUser user = (SecurityUser) auth.getPrincipal();
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
ResponseUtil.out(res, R.ok().data("token", token));
}
/**
* 登錄失敗
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
(2)TokenAuthenticationFilter:
授權filter
package com.atguigu.serurity.filter;
import com.atguigu.commonutils.R;
import com.atguigu.commonutils.utils.ResponseUtil;
import com.atguigu.serurity.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
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.Collection;
import java.util.List;
/**
* <p>
* 訪問過濾器
* </p>
*/
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) {
super(authManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
logger.info("================="+req.getRequestURI());
if(req.getRequestURI().indexOf("admin") == -1) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = null;
try {
authentication = getAuthentication(req);
} catch (Exception e) {
ResponseUtil.out(res, R.error());
}
if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
ResponseUtil.out(res, R.error());
}
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// token置於header裏
String token = request.getHeader("token");
if (token != null && !"".equals(token.trim())) {
String userName = tokenManager.getUserFromToken(token);
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
if (!StringUtils.isEmpty(userName)) {
return new UsernamePasswordAuthenticationToken(userName, token, authorities);
}
return null;
}
return null;
}
}
02-創建查詢用戶類和前端對接
一、創建自定義查詢用戶類
(1)在service_acl模塊創建,因爲其他模板不會用到
二、後端接口和前端頁面對接
1、在前端項目中下載依賴
npm install --save vuex-persistedstate
2、替換相關文件
3、在node_modules文件夾中替換element-ui依賴
Nacos配置中心
一、配置中心介紹
1、Spring Cloud Config
Spring Cloud Config 爲分佈式系統的外部配置提供了服務端和客戶端的支持方案。在配置的服務端您可以在所有環境中爲應用程序管理外部屬性的中心位置。客戶端和服務端概念上的Spring Environment 和 PropertySource 抽象保持同步, 它們非常適合Spring應用程序,但是可以與任何語言中運行的應用程序一起使用。當應用程序在部署管道中從一個開發到測試直至進入生產時,您可以管理這些環境之間的配置,並確保應用程序在遷移時具有它們需要運行的所有內容。服務器存儲後端的默認實現使用git,因此它很容易支持標記版本的配置環境,並且能夠被管理內容的各種工具訪問。很容易添加替代的實現,並用Spring配置將它們插入。
Spring Cloud Config 包含了Client和Server兩個部分,server提供配置文件的存儲、以接口的形式將配置文件的內容提供出去,client通過接口獲取數據、並依據此數據初始化自己的應用。Spring cloud使用git或svn存放配置文件,默認情況下使用git。
2、Nacos替換Config
Nacos 可以與 Spring, Spring Boot, Spring Cloud 集成,並能代替 Spring Cloud Eureka, Spring Cloud Config。通過 Nacos Server 和 spring-cloud-starter-alibaba-nacos-config 實現配置的動態變更。
(1)應用場景
在系統開發過程中,開發者通常會將一些需要變更的參數、變量等從代碼中分離出來獨立管理,以獨立的配置文件的形式存在。目的是讓靜態的系統工件或者交付物(如 WAR,JAR 包等)更好地和實際的物理運行環境進行適配。配置管理一般包含在系統部署的過程中,由系統管理員或者運維人員完成。配置變更是調整系統運行時的行爲的有效手段。
如果微服務架構中沒有使用統一配置中心時,所存在的問題:
-
配置文件分散在各個項目裏,不方便維護
-
配置內容安全與權限
-
更新配置後,項目需要重啓
nacos配置中心:系統配置的集中管理(編輯、存儲、分發)、動態更新不重啓、回滾配置(變更管理、歷史版本管理、變更審計)等所有與配置相關的活動。
二、讀取Nacos配置中心的配置文件
1、在Nacos創建統一配置文件
(1)點擊創建按鈕
(2)輸入配置信息
a)Data ID 的完整規則格式如下 $ {prefix}-$ {spring.profile.active}.$ {file-extension} - prefix 默認爲所屬工程配置spring.application.name 的值(即:nacos-provider),也可以通過配置項 spring.cloud.nacos.config.prefix來配置。- spring.profiles.active=dev 即爲當前環境對應的 profile。 注意:當 spring.profiles.active 爲空時,對應的連接符 - 也將不存在,dataId 的拼接格式變成 $ {prefix}.${file-extension}- file-exetension 爲配置內容的數據格式,可以通過配置項 spring.cloud.nacos.config.file-extension 來配置。目前只支持 properties 和 yaml 類型。
2、以service-statistics模塊爲例
(1)在service中引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
(2)創建bootstrap.properties配置文件
#配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
#spring.profiles.active=dev
# 該配置影響統一配置中心中的dataId
spring.application.name=service-statistics
(3)把項目之前的application.properties內容註釋,啓動項目查看效果
3、補充:springboot配置文件加載順序
其實yml和properties文件是一樣的原理,且一個項目上要麼yml或者properties,二選一的存在。推薦使用yml,更簡潔。
bootstrap與application
(1)加載順序
這裏主要是說明application和bootstrap的加載順序。
bootstrap.yml(bootstrap.properties)先加載
application.yml(application.properties)後加載
bootstrap.yml 用於應用程序上下文的引導階段。
bootstrap.yml 由父Spring ApplicationContext加載。
父ApplicationContext 被加載到使用 application.yml 的之前。
(2)配置區別
bootstrap.yml 和application.yml 都可以用來配置參數。
bootstrap.yml 可以理解成系統級別的一些參數配置,這些參數一般是不會變動的。
application.yml 可以用來定義應用級別的。
三、名稱空間切換環境
在實際開發中,通常有多套不同的環境(默認只有public),那麼這個時候可以根據指定的環境來創建不同的 namespce,例如,開發、測試和生產三個不同的環境,那麼使用一套 nacos 集羣可以分別建以下三個不同的 namespace。以此來實現多環境的隔離。
1、創建命名空間
默認只有public,新建了dev、test和prod命名空間
2、克隆配置
**(1)切換到配置列表:
可以發現有四個名稱空間:public(默認)以及我們自己添加的3個名稱空間(prod、dev、test),可以點擊查看每個名稱空間下的配置文件,當然現在只有public下有一個配置。默認情況下,項目會到public下找 服務名.properties文件。接下來,在dev名稱空間中也添加一個nacos-provider.properties配置。這時有兩種方式:第一,切換到dev名稱空間,添加一個新的配置文件。缺點:每個環境都要重複配置類似的項目第二,直接通過clone方式添加配置,並修改即可。推薦
點擊編輯:修改配置內容,端口號改爲8013以作區分
在項目模塊中,修改bootstrap.properties添加如下配置**
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.profiles.active=dev
# 該配置影響統一配置中心中的dataId,之前已經配置過
spring.application.name=service-statistics
spring.cloud.nacos.config.namespace=13b5c197-de5b-47e7-9903-ec0538c9db01
namespace的值爲:
重啓服務提供方服務,測試修改之後是否生效
四、多配置文件加載
在一些情況下需要加載多個配置文件。假如現在dev名稱空間下有三個配置文件:service-statistics.properties、redis.properties、jdbc.properties
添加配置,加載多個配置文件
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.profiles.active=dev
# 該配置影響統一配置中心中的dataId,之前已經配置過
spring.application.name=service-statistics
spring.cloud.nacos.config.namespace=13b5c197-de5b-47e7-9903-ec0538c9db01
spring.cloud.nacos.config.ext-config[0].data-id=redis.properties
# 開啓動態刷新配置,否則配置文件修改,工程無法感知
spring.cloud.nacos.config.ext-config[0].refresh=true
spring.cloud.nacos.config.ext-config[1].data-id=jdbc.properties
spring.cloud.nacos.config.ext-config[1].refresh=true