SpringBoot+MyBatis-plus+SpringSecurity+JWT 登入认证,实现前后端分离
1、SpringSecurity简要
- 认证 (你是谁)
- 授权 (你能干什么)
- 攻击防护 (防止伪造身份)
其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。
2、JWT 简要
-
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
-
JSON Web Token由三部分组成,它们之间用圆点(.)连接
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
3、本文代码执行流程图大致如下
4、核心功能代码
pom.xml 依赖配置:
<!-- 集成springsecurity 安全框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 集成jwt 框架 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
5、springsecurity 核心代码
JwtUserDetails
类说明:继承 UserDetails 权限判断属性( Security框架)
package com.digipower.sercurity.entity;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@SuppressWarnings("serial")
public class JwtUserDetails implements UserDetails {
private String username;
private String userPin;
private String password;
private String sid;
private Collection<? extends GrantedAuthority> authorities;
public String getUserPin() {
return userPin;
}
public void setUserPin(String userPin) {
this.userPin = userPin;
}
public String getSid() {
return sid;
}
public void setSid(String sid) {
this.sid = sid;
}
public JwtUserDetails() {
super();
// TODO Auto-generated constructor stub
}
public JwtUserDetails(String username, String password, String userPin, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.userPin = userPin;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
// 账户是否未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
// 账户是否未被锁
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetailServiceImpl
类说明:继承 UserDetailsService ,登录认证方法,由SecurityConfig 来配置指定此类来认证
package com.digipower.sercurity.userservice;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.digipower.entity.UcasAuthUserInfo;
import com.digipower.sercurity.entity.JwtUserDetails;
import com.digipower.service.UcasAuthUserInfoService;
@Component("userDetailServiceImpl")
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UcasAuthUserInfoService ucasAuthUserInfoService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO Auto-generated method stub
// 根据用户名去查找用户信息
QueryWrapper<UcasAuthUserInfo> queryWrapper = new QueryWrapper<UcasAuthUserInfo>();
queryWrapper.eq("user_pin", username);
List<UcasAuthUserInfo> list = ucasAuthUserInfoService.list(queryWrapper);
if (CollectionUtils.isEmpty(list)) {
throw new UsernameNotFoundException(String.format("Not user Found with '%s'", username));
}
UcasAuthUserInfo customer = list.get(0);
return new JwtUserDetails(customer.getUserName(), customer.getPassword(), customer.getUserPin(), getGrantedAuthority());
}
/**
* 用户登入成功默认所有角色权限
* @return
*/
private List<GrantedAuthority> getGrantedAuthority() {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("all"));
return authorities;
}
}
WebSecurityConfig配置类
package com.digipower.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.password.PasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import com.digipower.sercurity.filter.JWTLoginFilter;
import com.digipower.sercurity.filter.JWTValidFilter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailServiceImpl")
private UserDetailsService userDetailService;
/**
* 认证
*
* @return
*/
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
//对默认的UserDetailsService进行覆盖
authenticationProvider.setUserDetailsService(userDetailService);
authenticationProvider.setPasswordEncoder(new PasswordEncoder() {
// 对密码未加密
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
// 判断密码是否正确, rawPassword 用户输入的密码, encodedPassword 数据库DB的密码,当 userDetailService的loadUserByUsername方法执行完后执行
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equalsIgnoreCase(encodedPassword);
}
});
return authenticationProvider;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilter(new JWTValidFilter(authenticationManager()));
http.addFilter(new JWTLoginFilter(authenticationManager())).csrf().disable();
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth/login").permitAll()
.antMatchers("/swagger-ui.html/**").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/v2/api-docs/**").permitAll()
.anyRequest().authenticated(); // 任何请求,登录后可以访问
// 开启跨域访问
http.cors().disable();
// 开启模拟请求,比如API POST测试工具的测试,不开启时,API POST为报403错误
http.csrf().disable();
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
CorsConfiguration cors = new CorsConfiguration();
cors.setAllowCredentials(true);
cors.addAllowedOrigin("*");
cors.addAllowedHeader("*");
cors.addAllowedMethod("*");
configurationSource.registerCorsConfiguration("/**", cors);
return new CorsFilter(configurationSource);
}
}
6、JWT 核心代码
JWTLoginFilter
类说明:用户自定义登入拦截器、处理用户登入方法、登入成功方法和登入失败方法
package com.digipower.sercurity.filter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.digipower.common.entity.Result;
import com.digipower.sercurity.entity.JwtUserDetails;
import com.digipower.sercurity.util.JwtTokenUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
/***
* TODO 登录 ===> POST请求( 账号:username=?, 密码:password=?)
*
* 登录会调用springSecurity的登录方法进行验证
*<p>
* ===== 登录成功
* http状态status状态返回200,并且自定义响应状态code返回200,响应头存放token,key = token,value = jwt生成的token内容
* ===== 登录失败
* http状态status状态返回401,并且自定义响应状态code返回401,并提示对应的内容
* ===== 权限不足
* http状态status状态返回403,并且自定义响应状态code返回403,并提示对应的内容
* </p>
* @author zzg
*/
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
/**
* 获取授权管理, 创建JWTLoginFilter时获取
*/
private AuthenticationManager authenticationManager;
/**
* 创建JWTLoginFilter,构造器,定义后端登陆接口-【/auth/login】,当调用该接口直接执行 attemptAuthentication 方法
*
* @param authenticationManager
*/
public JWTLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/auth/login");
}
/**
* TODO 一旦调用登录接口 /auth/login,立即执行该方法
*
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
JwtUserDetails user = null;
ObjectMapper objectMapper = new ObjectMapper();
try {
user = new ObjectMapper().readValue(request.getInputStream(), JwtUserDetails.class);
} catch (IOException e) {
try{
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(Result.error("401","没有传递对应的参数")));
} catch(Exception message){
}
return null;
}
// 调用springSecurity的 XiJiaUserDetailsServiceImpl 的 loadUserByUsername 方法进行登录认证,传递账号密码
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
}
/**
* TODO 一旦调用 springSecurity认证登录成功,立即执行该方法
*
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
// 生成jwt,并返回
JwtUserDetails userEntity = (JwtUserDetails) authResult.getPrincipal();
String jwtToken = JwtTokenUtil.generateToken(userEntity);
ObjectMapper objectMapper = new ObjectMapper();
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream out = response.getOutputStream();
String str = objectMapper.writeValueAsString(Result.ok().setDatas("token", jwtToken));
out.write(str.getBytes("UTF-8"));
out.flush();
out.close();
}
/**
* TODO 一旦调用 springSecurity认证失败 ,立即执行该方法
*
* @param request
* @param response
* @param ex
* @throws IOException
* @throws ServletException
*/
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) {
ObjectMapper objectMapper = new ObjectMapper();
try{
if (ex instanceof UsernameNotFoundException || ex instanceof BadCredentialsException) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(Result.error("401","用户名或密码错误")));
} else if (ex instanceof InternalAuthenticationServiceException) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(Result.error("401","没有账号信息")));
} else if (ex instanceof DisabledException) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(Result.error("401","账户被禁用")));
} else {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(Result.error("401","登录失败!")));
}
} catch(Exception e){
}
}
}
JWTValidFilter
类说明:用户登入成功后,携带token 访问其他数据接口凭证判断拦截器
package com.digipower.sercurity.filter;
import java.io.IOException;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import com.digipower.common.entity.Result;
import com.digipower.sercurity.util.JwtTokenUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ExpiredJwtException;
public class JWTValidFilter extends BasicAuthenticationFilter {
/**
* SecurityConfig 配置中创建该类实例
*/
public JWTValidFilter(AuthenticationManager authenticationManager) {
// 获取授权管理
super(authenticationManager);
}
/**
* 拦截请求
*
* @param request
* @param response
* @param chain
* @throws IOException
* @throws ServletException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
ObjectMapper objectMapper = new ObjectMapper();
// 获取token, 没有token直接放行
String token = request.getHeader("token");
if (StringUtils.isBlank(token) || "null".equals(token)) {
super.doFilterInternal(request, response, chain);
return;
}
// 有token进行权限验证
String username = null;
try {
// 获取账号
username = JwtTokenUtil.getUsername(token);
} catch (ExpiredJwtException ex) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(Result.error("10000","登录过期")));
return;
} catch (Exception e) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(Result.error("10000","JWT解析错误")));
return;
}
// 添加账户的权限信息,和账号是否为空,然后保存到Security的Authentication授权管理器中
if (StringUtils.isNotBlank(username)) {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(username, null, new ArrayList<SimpleGrantedAuthority>()));
}
super.doFilterInternal(request, response, chain);
}
}
JwtTokenUtil
类说明:jwt 工具类
package com.digipower.sercurity.util;
import java.util.Date;
import org.springframework.stereotype.Component;
import com.digipower.sercurity.entity.JwtUserDetails;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
/**
* jwt 工具类
*
* @author zzg
*
*/
public class JwtTokenUtil {
// 主题
private static final String SUBJECT = "digipower";
// jwt的token有效期,
//private static final long EXPIRITION = 1000L * 60 * 60 * 24 * 7;//7天
private static final long EXPIRITION = 1000L * 60 * 30; // 半小时
// 加密key(黑客没有该值无法篡改token内容)
private static final String APPSECRET_KEY = "digipower";
// 用户url权限列表key
private static final String AUTH_CLAIMS = "auth";
/**
* TODO 生成token
*
* @param user
* @return java.lang.String
* @date 2020/7/6 0006 9:26
*/
public static String generateToken(JwtUserDetails user) {
String token = Jwts
.builder()
// 主题
.setSubject(SUBJECT)
// 添加jwt自定义值
.claim(AUTH_CLAIMS, user.getAuthorities())
.claim("username", user.getUsername())
.claim("userPin", user.getUserPin())
.claim("sid", user.getSid())
.setIssuedAt(new Date())
// 过期时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
// 加密方式,加密key
.signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
return token;
}
/**
* 获取用户Id
*
* @param token
* @return
*/
public static String getUserId(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("sid").toString();
}
/**
* 获取用户名
*
* @param token
* @return
*/
public static String getUsername(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("username").toString();
}
/**
* 是否过期
*
* @param token
* @return
*/
public static boolean isExpiration(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
System.out.println("过期时间: " + claims.getExpiration());
return claims.getExpiration().before(new Date());
}
}
7、SpringBoot + MyBatis-plus + Druid + MySQL8 项目搭建
请参考文章地址:SpringBoot + MyBatis-plus + Druid 实现简单增删查改、动态条件查询和分页功能