在微服務中使用SpringSercuity+JWT實現前後端分離的接口認證
項目是SpringBoot
pom文件如下
<dependencies>
<!-- SpringBoot整合Web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot整合Zuul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- SpringBoot整合eureka客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 獲取yml配置屬性 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-->spring-boot 整合security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-->spring-boot 整合JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-->Json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
<scope>compile</scope>
</dependency>
<!-->lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- SpringBoot整合fegnin客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
其中zuul網關的搭建使用我就不詳細說了,主要說一下SpringSercuity+JWT怎麼配合微服務項目
導入上面的包後,我們需要對SpringSercuity+JWT配置配置文件
SpringSercuity的配置需要以下幾個文件
1.新建WebSecurityConfigurer類集成SpringSercuity的WebSecurityConfigurerAdapter,重寫裏面的方法,代碼如下
package com.tm.zuul.config.security;
import com.tm.zuul.config.jwt.JwtAuthenticationEntryPoint;
import com.tm.zuul.config.jwt.JwtAuthenticationTokenFilter;
import com.tm.zuul.config.jwt.RestAccessDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity//開啓Security
@EnableGlobalMethodSecurity(prePostEnabled = true)//實現角色對某個操作是否有權限的控制
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private RestAccessDeniedHandler restAccessDeniedHandler;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
//使用自己的前置攔截器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 定製我們自己的 session 策略:調整爲讓 Spring Security 不創建和使用 session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 請求進行攔截 驗證 accessToken
http
.authorizeRequests()
//指定需要攔截的uri
.antMatchers("/api-user/web/**").authenticated()
///其他請求都可以訪問
.anyRequest().permitAll()
.and().exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)//身份訪問異常
.accessDeniedHandler(restAccessDeniedHandler)//權限訪問異常
//解決跨域
.and()
.cors()
// 關閉csrf防護
.and()
.csrf()
.disable();
}
@Bean
protected UserDetailsService customUserService() {//註冊UserDetailsService的bean
return new CustomUserService();
}
//SpringBoot2.x後需要使用BCrypt強哈希方法來加密密碼,如果不加的話登錄不上並且控制檯會有警告Encoded password does not look like BCrypt
@Bean
public BCryptPasswordEncoder PasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService()).passwordEncoder(PasswordEncoder());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
這個文件是最核心的,其中
1.JwtAuthenticationEntryPoint
當用戶沒有獲取token就訪問需要認證token的接口時
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
System.out.println("JwtAuthenticationEntryPoint:" + authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "沒有憑證");
}
}
2.RestAccessDeniedHandler
當用戶訪問沒有權限時回調的接口
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
System.out.println("RestAccessDeniedHandler:" + e.getMessage());
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "拒絕訪問");
}
}
3.JwtAuthenticationTokenFilter
繼承 OncePerRequestFilter 實現攔截器,上面的配置是訪問地址爲/api-user/web/**的用戶需要校驗權限,校驗的時候我們就會走到這個過濾器裏面來判斷JWT的token信息,進行相應的處理。裏面還需要引入JWT的token管理類。這個JWT直接使用Sercuity的用戶信息進行生成。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Qualifier("customUserService")
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String token = request.getHeader(jwtTokenUtil.getHeader());//獲取token
if (!StringUtils.isEmpty(token)) {//判斷token是否爲空
String username = jwtTokenUtil.getUsernameFromToken(token);//取出token的用戶信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {//判斷Security的用戶認證信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(token, userDetails)) {//把前端傳遞的Token信息與當前的Security的用戶信息進行校驗
// 將用戶信息存入 authentication,方便後續校驗
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 將 authentication 存入 ThreadLocal,方便後續獲取用戶信息
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
/**
* 生成令牌,驗證等等一些操作
*
*/
@Data
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
// 過期時間 毫秒
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.header}")
private String header;
/**
* 從數據聲明生成令牌
*
* @param claims 數據聲明
* @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + expiration);
return Jwts.builder()
.setClaims(claims)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 從令牌中獲取數據聲明
*
* @param token 令牌
* @return 數據聲明
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成令牌
*
* @param userDetails 用戶
* @return 令牌
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(2);
claims.put(Claims.SUBJECT, userDetails.getUsername());
claims.put(Claims.ISSUED_AT, new Date());
return generateToken(claims);
}
/**
* 從令牌中獲取用戶名
*
* @param token 令牌
* @return 用戶名
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判斷令牌是否過期
*
* @param token 令牌
* @return 是否過期
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return true;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put(Claims.ISSUED_AT, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 驗證令牌
*
* @param token 令牌
* @param userDetails 用戶
* @return 是否有效
*/
public Boolean validateToken(String token, UserDetails userDetails) {
SecurityUser user = (SecurityUser) userDetails;
String username = getUsernameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
}
jwt:
secret: secret
expiration: 604800
header: Authorization
上面代碼中在SecurityUser是CustomUserService裏面的實體工具類。
4.CustomUserService
繼承UserDetailsService實現將用戶信息,與用戶的角色信息返回給SpringSercuity
@Component("customUserService")
public class CustomUserService implements UserDetailsService {
@Autowired
private IUserServiceFegin userServiceFegin;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Response response = userServiceFegin.queryManagerUserByEmail(s);
if (response == null) {
System.out.println("查詢空");
return null;
}
if (response.getCode() == ResponseCode.SUCCESS.getCode()) {
try {
//轉化
String s1 = JSON.toJSONString(response);
UserLoginEntity loginEntity = JSON.parseObject(s1, UserLoginEntity.class);
//用戶信息處理
SecurityUser securityUser = new SecurityUser();
securityUser.setEmail(loginEntity.getData().getManagerUser().getEmail());
securityUser.setPassword(String.valueOf(loginEntity.getData().getManagerUser().getPassword()));
securityUser.setSign(loginEntity.getData().getManagerUser().isSign());
//角色處理
List<SecurityRole> roleList = new ArrayList<>();
List<UserLoginEntity.DataBean.RoleListBean> roleListBeans = loginEntity.getData().getRoleList();
for (UserLoginEntity.DataBean.RoleListBean roleListBean : roleListBeans) {
SecurityRole securityRole = new SecurityRole();
securityRole.setName(roleListBean.getName());
securityRole.setCodeName(roleListBean.getCodeName());
roleList.add(securityRole);
}
securityUser.setRoleList(roleList);
return securityUser;
} catch (Exception e) {
System.out.println(e.getMessage());
return null;
}
} else {
System.out.println(response.getCode() + ":" + response.getMsg());
return null;
}
}
}
public class SecurityUser implements Serializable, UserDetails {
public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();
private static final long serialVersionUID = 1L;
/**
* 郵箱號碼
*/
private String email;
/**
* 登錄密碼
*/
private String password;
/**
* 使用狀態(0正常使用中)
*/
private Boolean sign;
private List<SecurityRole> roleList;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Boolean getSign() {
return sign;
}
public void setSign(Boolean sign) {
this.sign = sign;
}
public List<SecurityRole> getRoleList() {
return roleList;
}
public void setRoleList(List<SecurityRole> roleList) {
this.roleList = roleList;
}
public void setPassword(String password) {
this.password = PASSWORD_ENCODER.encode(password);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//將用戶角色作爲權限
List<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
List<SecurityRole> roles = this.getRoleList();
for (SecurityRole role : roles) {
System.out.println(role.getCodeName());
auths.add(new SimpleGrantedAuthority(role.getCodeName()));
}
return auths;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
//賬戶是否過期,過期無法驗證
@Override
public boolean isAccountNonExpired() {
return true;
}
//指定用戶是否被鎖定或者解鎖,鎖定的用戶無法進行身份驗證
@Override
public boolean isAccountNonLocked() {
return true;
}
//指示是否已過期的用戶的憑據(密碼),過期的憑據防止認證
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//是否被禁用,禁用的用戶不能身份驗證
@Override
public boolean isEnabled() {
return true;
}
}
public class SecurityRole {
private String name;
private String codeName;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCodeName() {
return codeName;
}
public void setCodeName(String codeName) {
this.codeName = codeName;
}
}
@Data
public class Response implements Serializable {
//返回碼
private int code;
//返回描述
private String msg;
//返回數據
private Object data;
public Response() {
}
//其他自定義
public Response(ResponseCode tmResponseCode, Object data) {
this.code = tmResponseCode.getCode();
this.msg = tmResponseCode.getMsg();
this.data = data;
}
//請求成功,不需要返回數據
public Response ResponseSucess() {
Response response = new Response();
response.code = ResponseCode.SUCCESS.getCode();
response.msg = ResponseCode.SUCCESS.getMsg();
return response;
}
//請求失敗,不需要返回數據
public Response ResponseError() {
Response response = new Response();
response.code = ResponseCode.ERROR.getCode();
response.msg = ResponseCode.ERROR.getMsg();
return response;
}
//請求失敗,需要返回數據
public Response ResponseErrorData(Object data) {
return new Response(ResponseCode.ERROR, data);
}
//請求成功,需要返回數據
public Response ResponseSucessData(Object data) {
return new Response(ResponseCode.SUCCESS, data);
}
}
public enum ResponseCode {
SUCCESS(10001, "成功"),
ERROR(10002, "失敗"),
ROUTE_ERROR(10003, "請求路徑不存在"),
SERVER_ERROR(10004, "服務器響應異常"),
TIMEOUT_ERROR(10005, "請求超時"),
PARAMS_ERROR(10006, "參數錯誤"),
SERVER_DOWNGRADE(10007, "服務降級");
private int code;
private String msg;
ResponseCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
package com.tm.zuul.feign.entity;
import java.util.List;
public class UserLoginEntity {
private int code;
private String msg;
private DataBean data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public DataBean getData() {
return data;
}
public void setData(DataBean data) {
this.data = data;
}
public static class DataBean {
private ManagerUserBean managerUser;
private List<RoleListBean> roleList;
private String token;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public ManagerUserBean getManagerUser() {
return managerUser;
}
public void setManagerUser(ManagerUserBean managerUser) {
this.managerUser = managerUser;
}
public List<RoleListBean> getRoleList() {
return roleList;
}
public void setRoleList(List<RoleListBean> roleList) {
this.roleList = roleList;
}
public static class ManagerUserBean {
private int id;
private String tel;
private String email;
private Object password;
private String name;
private String address;
private Object photo;
private boolean sign;
private String createtime;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTel() {
return tel;
}
public void setTel(String tel) {
this.tel = tel;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Object getPassword() {
return password;
}
public void setPassword(Object password) {
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Object getPhoto() {
return photo;
}
public void setPhoto(Object photo) {
this.photo = photo;
}
public boolean isSign() {
return sign;
}
public void setSign(boolean sign) {
this.sign = sign;
}
public String getCreatetime() {
return createtime;
}
public void setCreatetime(String createtime) {
this.createtime = createtime;
}
}
public static class RoleListBean {
private int id;
private int pid;
private String name;
private String codeName;
private Object sign;
private Object createtime;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCodeName() {
return codeName;
}
public void setCodeName(String codeName) {
this.codeName = codeName;
}
public Object getSign() {
return sign;
}
public void setSign(Object sign) {
this.sign = sign;
}
public Object getCreatetime() {
return createtime;
}
public void setCreatetime(Object createtime) {
this.createtime = createtime;
}
}
}
}
fegin客戶端代碼
public interface IUserApi {
//管理員登錄
@PostMapping("/user/managerUserLogin")
Response managerUserLogin(@RequestParam("username") String username, @RequestParam("password") String password);
//根據管理員郵箱查詢信息
@PostMapping("/user/queryManagerUserByEmail")
Response queryManagerUserByEmail(@RequestParam("username") String username);
}
@Component("userServiceFegin")
@FeignClient(value = "tm-fenghua-user", fallback = UserFallBack.class)
public interface IUserServiceFegin extends IUserApi {
}
/************************
* @作者 fenghua
* @創建日期 2019/8/5 0:44
* @功能 用戶服務降級信息返回
************************/
@Component
public class UserFallBack implements IUserServiceFegin {
@Override
public Response managerUserLogin(String username, String password) {
return new Response(ResponseCode.SERVER_DOWNGRADE, "服務降級");
}
@Override
public Response queryManagerUserByEmail(String username) {
return new Response(ResponseCode.SERVER_DOWNGRADE, "服務降級");
}
}
這也是主要的一個接口
@RestController
public class AuthController {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private CustomUserService customUserService;
@Autowired
private IUserServiceFegin userServiceFegin;
/**
* 網關登錄接口
*
* @param username
* @param password
* @return
*/
@RequestMapping("/login")
public Response login(String username, String password) {
Response response = userServiceFegin.managerUserLogin(username, password);
if (response == null) {
return new Response().ResponseErrorData("網關獲取對象數據失敗-1");
}
if (response.getCode() == ResponseCode.SUCCESS.getCode()) {
String s1 = JSON.toJSONString(response);
UserLoginEntity loginEntity = JSON.parseObject(s1, UserLoginEntity.class);
if (loginEntity.getData() == null) {
return new Response().ResponseErrorData("網關獲取對象數據失敗-2");
}
if (loginEntity.getData().getManagerUser() == null) {
return new Response().ResponseErrorData("網關獲取對象數據失敗-3");
}
if (StringUtils.isEmpty(loginEntity.getData().getManagerUser().getEmail())) {
return new Response().ResponseErrorData("網關獲取對象數據失敗-4");
}
UserDetails userDetails = customUserService.loadUserByUsername(loginEntity.getData().getManagerUser().getEmail());
String token = jwtTokenUtil.generateToken(userDetails);//獲取Token
loginEntity.getData().setToken(token);//設置Token
return new Response().ResponseSucessData(loginEntity.getData());
} else {
return response;
}
}
}
上面貼了這麼多代碼,可能需要有一定基礎或是用點心的才容易看懂。
現在說一下思路
在沒有權限攔截的時候,是直接訪問用戶服務的管理員登錄接口,就是下面這個
然後我們需要權限攔截,Token校驗,於是就需要在zuul網關層面對接口進行處理。
以前的登錄接口訪問的時候是沒有返回token的,就返回登錄用戶的詳細信息,登錄用戶的角色列表。
現在的登錄接口在網關通過遠程接口先請求用戶服務接口判斷用戶的賬戶,密碼是否正確後,然後返回成功的信息回網關,網關通過返回的用戶信息,關聯Sericuty,如下
然後進入到下面這個方法,這個方法又通過遠程服務接口查詢用戶的具體信息,用戶名,密碼,狀態,角色列表等與Sericuty進行綁定。
然後就從Sericuty裏面取用戶的信息生成Token
然後再把封裝好的信息返回給用戶端,用戶端在訪問需要Token驗證的接口時就能把token傳遞過來進行認證訪問。