SpringSecurity使用JWT实现单点登录-MY
流程
父工程pom.xml
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<lombok.version>1.18.8</lombok.version>
<mysql.version>5.1.49</mysql.version>
<mybatis-plus.version>3.4.0</mybatis-plus.version>
<swagger.version>2.9.2</swagger.version>
</properties>
<modules>
<module>tnqf-auth</module>
</modules>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
子工程pom.xml
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
SwaggerConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@EnableSwagger2
@Configuration
public class SwaggerConfig {
@Bean
public Docket getUserDocket(){
ApiInfo apiInfo = new ApiInfoBuilder()
.title("auth认证测试")
.description("永无BUG")
.build();
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.yjxluser.controller"))
.paths(PathSelectors.any())
.build();
}
}
apiInfo的title,description是在swagger-ui.html上显示的。
apis(RequestHandlerSelectors.basePackage("com.example.yjxluser.controller")),接口扫描,处理指定的Controller。
.paths(PathSelectors.any()),匹配在controller package中的所有url。
Spring Security概览
在Spring Security中,通过将FilterChainProxy嵌入到SpringMVC的过滤器链达到过滤目的。
FilterChainProxy管理Spring Security中所有默认过滤器。
请求到达FilterChainProxy之后,根据Filter的优先级匹配,匹配哪个用哪个处理。
JSON登录
UsernamePasswordAuthenticationFilter登录
UsernamePasswordAuthenticationFilter属于过滤器链的一环,此过滤器的作用是对前端传递过来的用户名密码进行拦截解析,但是只拦截指定的url,例如“/login”。由于默认仅支持form表单登录,所以需要自定义一个可以接收json的UsernamePasswordAuthenticationFilter,以此来满足前后端分离的需求。
MyUsernamePasswordAuthenticationFilter.java
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
// 判断前端传递的是否是json
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)){
Map<String, String> loginData = new HashMap<>();
try {
loginData = new ObjectMapper().readValue(request.getInputStream(), HashMap.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 通过UsernamePasswordAuthenticationFilter.getUsernameParameter()获取key名称
String username = loginData.get(getUsernameParameter()).trim();
String password= loginData.get(getPasswordParameter());
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}else {
return super.attemptAuthentication(request,response);
}
}
}
}
过滤器的指定需要在SecurityConfig(自定义的)中指定,SecurityConfig继承自WebSecurityConfigurerAdapter。
SecurityConfig.java
import com.example.yjxluser.filter.MyUsernamePasswordAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 放行swagger 资源
http.authorizeRequests()
.antMatchers("/swagger-ui.html",
"/swagger-resources/**",
"/webjars/**",
"/v2/**",
"/api/**")
.permitAll();
http.authorizeRequests()
.anyRequest().authenticated();
// 替换过滤器
http.addFilterAt(myUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.csrf().disable();
http.cors().disable();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter() throws Exception {
MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter = new MyUsernamePasswordAuthenticationFilter();
myUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/login_handler");
myUsernamePasswordAuthenticationFilter.setUsernameParameter("username");
myUsernamePasswordAuthenticationFilter.setPasswordParameter("password");
myUsernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
return myUsernamePasswordAuthenticationFilter;
}
}
替换了UsernamePasswordAuthenticationFilter之后,默认的form登录会失效,失效的属性可以在MyUsernamePasswordAuthenticationFilter中重新配置,必须用setAuthenticationManager()方法配置AuthenticationManager,否则启动报错。
UserController
@RequestMapping("/login_handler")
public void loginHandler(){
}
此处/login_handler不需要处理逻辑,登录逻辑在UserDetailsService中处理,成功登录之后再AuthenticationSuccessHandler中处理。
JWT
application.yml
my:
jwt:
expire: 604800
secret: qaz9988
header: Authorization
JWTUtils.java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
@Data
@Component
@ConfigurationProperties(prefix = "my.jwt")
public class JwtUtils {
private long expire;
private String secret;
private String header;
public String generateToken(String username){
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + 1000 * expire);
return Jwts.builder()
.setHeaderParam("type","JWT")
.setSubject(username)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512,secret)
.compact();
}
// 解析JWT
public Claims getClaimsByToken(String jwt) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jwt)
.getBody();
} catch (Exception e) {
return null;
}
}
// 判断JWT是否过期
public boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
}
UserDetailsService
UserDetailsService使用loadUserByUsername方法查询数据库返回UserDetails,UserDetailsServiceImpl实现UserDetailsService。
import com.example.yjxluser.entity.MyUserDetails;
import com.example.yjxluser.mapper.UserInfoMapper;
import com.example.yjxluser.mapper.UserPasswordMapper;
import com.example.yjxluser.service.model.UserModel;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;
import java.util.Optional;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserInfoMapper userInfoMapper;
@Autowired
private UserPasswordMapper userPasswordMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserModel userModel = new UserModel();
return Optional.ofNullable(userInfoMapper.getByUsername(username))
.map(u->{
BeanUtils.copyProperties(u,userModel);
return userModel;
})
.flatMap(um->Optional.ofNullable(userPasswordMapper.getByUid(um.getId())))
.map(p->{
userModel.setPassword(p.getPassword());
return MyUserDetails.builder()
.username(userModel.getUsername())
.password(userModel.getPassword())
.build();
})
.orElse(null);
}
}
UserModel
核心领域模型
import com.example.yjxluser.entity.Contact;
import com.example.yjxluser.entity.Roles;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserModel {
private Long id;
private String username;
private String trueName;
private java.sql.Timestamp createName;
private Long enable;
private String phoneNumber;
private Long gender;
private java.sql.Timestamp birthday;
private String location;
private String password;
private String salt;
private List<Roles> roles;
private List<Contact> contacts;
}
UserDetails
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MyUserDetails implements UserDetails {
private String username;
private String password;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.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;
}
}
JWTAuthenticationFilter
import com.example.yjxluser.mapper.UserInfoMapper;
import com.example.yjxluser.mapper.UserPasswordMapper;
import com.example.yjxluser.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JWTAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserPasswordMapper userPasswordMapper;
@Autowired
private UserInfoMapper userInfoMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String jwt = request.getHeader(jwtUtils.getHeader());
if (StringUtils.isBlank(jwt)){
chain.doFilter(request,response);
return;
}
// 解析前端传递jwt
Claims claimsByToken = jwtUtils.getClaimsByToken(jwt);
// 若为空则无法解析,jwt是被篡改了
if (claimsByToken==null){
throw new JwtException("异常");
}
if (jwtUtils.isTokenExpired(claimsByToken)){
throw new JwtException("jwtToken过期");
}
// 获取jwt解析的用户名
String username = claimsByToken.getSubject();
// 没权限也要必须设置为null,不能不填,否则认证失败
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(
username,
userPasswordMapper.getByUid(userInfoMapper.getByUsername(username).getId()),
null
);
// 将构建的UsernamePasswordAuthenticationToken放入security的上下文
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request,response);
}
}
SecurityConfig.java
import com.example.yjxluser.filter.JWTAuthenticationFilter;
import com.example.yjxluser.filter.MyUsernamePasswordAuthenticationFilter;
import com.example.yjxluser.utils.JwtUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.PrintWriter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService myUserDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 放行swagger 资源
http.authorizeRequests()
.antMatchers("/swagger-ui.html",
"/swagger-resources/**",
"/webjars/**",
"/v2/**",
"/api/**")
.permitAll();
http.authorizeRequests().anyRequest().authenticated();
// 替换默认UsernamePasswordAuthenticationFilter
http.addFilterAt(myUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// jwt要优先验证
// 添加jwtAuthenticationFilter在MyUsernamePasswordAuthenticationFilter之前
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// form表单登录必须关闭,否则会登录之后还会要求验证
// http.authorizeRequests()
// .anyRequest().authenticated()
// .and()
// .formLogin()
// .loginProcessingUrl("/login_handler")
// .successForwardUrl("/login_result")
// .permitAll();
//关闭session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable();
http.cors().disable();
}
// 设置自定义UserDetailsService和PasswordEncoder
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter() throws Exception {
MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter = new MyUsernamePasswordAuthenticationFilter();
// 处理登录url
myUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/login_handler");
// 设置登录成功处理器
myUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler((httpServletRequest, httpServletResponse, authentication) -> {
PrintWriter writer = httpServletResponse.getWriter();
writer.write(new ObjectMapper().writeValueAsString(jwtUtils.generateToken(authentication.getName())));
writer.flush();
writer.close();
});
// 设置获取前端用户名和密码参数名称
myUsernamePasswordAuthenticationFilter.setUsernameParameter("username");
myUsernamePasswordAuthenticationFilter.setPasswordParameter("password");
// 必须设置AuthenticationManager,否则报错。AuthenticationManager管理Authentication。
myUsernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
return myUsernamePasswordAuthenticationFilter;
}
@Bean
public JWTAuthenticationFilter jwtAuthenticationFilter() throws Exception {
return new JWTAuthenticationFilter();
}
}