在微服务中使用SpringSercuity+JWT实现前后端分离的接口认证
项目是SpringBoot
pom文件如下
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-zuul
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-autoconfigure
spring-boot 整合security -->
org.springframework.boot
spring-boot-starter-security
spring-boot 整合JWT -->
io.jsonwebtoken
jjwt
0.9.1
Json-->
com.alibaba
fastjson
1.2.58
compile
lombok-->
org.projectlombok
lombok
org.springframework.cloud
spring-cloud-starter-openfeign
其中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 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 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 roleList = new ArrayList<>();
List 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 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 getRoleList() {
return roleList;
}
public void setRoleList(List roleList) {
this.roleList = roleList;
}
public void setPassword(String password) {
this.password = PASSWORD_ENCODER.encode(password);
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
//将用户角色作为权限
List auths = new ArrayList();
List 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 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 getRoleList() {
return roleList;
}
public void setRoleList(List 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传递过来进行认证访问。