SpringBoot + MyBatis-plus + SpringSecurity + JWTログイン認証により、フロントエンドとバックエンドを分離します
1.春のセキュリティブリーフ
- 認定(あなたは誰ですか)
- 承認(あなたは何ができますか)
- 攻撃保護(身分証明書の偽造の防止)
の中核は、一連のフィルターチェーンであり、プロジェクトの開始後に自動的に構成されます。コアは、ユーザーのIDを認証するために使用される基本認証フィルターであり、SpringSecurityのフィルターが認証方法を処理します。
2.JWTブリーフ
-
JSON Web Token(JWT)はオープンスタンダード(RFC 7519)であり、当事者間で情報をJSONオブジェクトとして安全に送信するためのコンパクトな自己完結型の方法を定義しています。この情報はデジタル署名されているため、検証および信頼できます。
-
JSON Web Tokenは3つの部分で構成されています。
最初の部分はドット(。)で接続されています。これをヘッダーと呼びます。2番目の部分はペイロード(ペイロード、飛行機で運ばれるアイテムと同様)と呼ばれます。3番目の部分はビザ(署名)。
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権限判断属性を継承します(セキュリティフレームワーク)
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
クラスの説明:ユーザーが正常にログインした後、トークンを携帯して他のデータインターフェイスの資格情報にアクセスし、インターセプターを決定します
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は、簡単な追加、削除、チェック、動的条件クエリ、およびページング機能を実現します。
8.郵便配達員がデモを要求する
Githubアドレス:https://github.com/zhouzhiwengang/baoan-house