引言
一般业务开发中, 我们会新建一个网关系统用于对接移动端相关的接口。比如H5,小程序,APP。在对接的过程中首先需要解决的问题就是实现多种登陆方式。 比如用户名密码,手机验证码登陆,微信授权登陆,人脸识别登陆等。 目前主流做法就是使用Security,Shiro成熟的框架进行实现。 接下来就来总结相关实现步骤。
环境准备
- maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
- application.yml
jwt:
secret: my_secret_2019 # 签名密钥
expiration: 604800 # jwt有效期(一天:86400,一周 604800)
代码方案
- JwtFilter
import com.loan.help.crm.modules.user.model.AuthUser;
import com.loan.help.crm.security.wechat.WeChatAuthenticationToken;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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;
/**
* 这个类在配置文件已经完成了容器的注入SecurityConfig
* @author mark1xie
*/
//@Component
@Slf4j
public class JwtAuthFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
private String tokenHeader = "Authorization";
private String tokenPrefix = "Bearer";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 从http头部读取jwt
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(tokenPrefix)) {
// The part after "Bearer "
final String authToken = authHeader.substring(tokenPrefix.length() + 1);
String username = null, role = null;
// 从jwt中解出账号与角色信息
try {
Long start = System.currentTimeMillis();
username = jwtUtil.getUsernameFromToken(authToken);
log.info("username cost:{}", System.currentTimeMillis() - start);
role = jwtUtil.getClaimFromToken(authToken, "role", String.class);
} catch (Exception e) {
log.debug("异常详情", e);
log.info("无效token");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"status\":403,\"message\":\"无效token\"}");
return;
}
// 如果jwt正确解出账号信息,说明是合法用户,设置认证信息,认证通过
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
long start = System.currentTimeMillis();
// UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
// username, null, AuthUser.getAuthoritiesByRole(role));
WeChatAuthenticationToken auth = new WeChatAuthenticationToken(username, AuthUser.getAuthoritiesByRole(role));
// 把请求的信息设置到UsernamePasswordAuthenticationToken details对象里面,包括发请求的ip等
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置认证信息
SecurityContextHolder.getContext().setAuthentication(auth);
log.info("auth cost:{}", System.currentTimeMillis() - start);
}
}
// 调用下一个过滤器
chain.doFilter(request, response);
}
}
- SecurityConfig
import com.loan.help.crm.security.jwt.JwtAuthError;
import com.loan.help.crm.security.jwt.JwtAuthFilter;
import com.loan.help.crm.security.sms.MobileCodeAuthenticationProvider;
import com.loan.help.crm.security.wechat.WeChatAuthenticationProvider;
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.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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
/**
* @author mark1xie
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MobileCodeAuthenticationProvider mobileCodeAuthenticationProvider;
@Autowired
private WeChatAuthenticationProvider weChatAuthenticationProvider;
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
/**
* 权限不足错误信息处理,包含认证错误与鉴权错误处理
*/
@Autowired
private JwtAuthError myAuthErrorHandler;
/**
* 加载用户信息
*/
@Autowired
private UserDetailsService myUserDetailsService;
/**
* 密码明文加密方式配置
*/
@Bean
public PasswordEncoder myEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 获取AuthenticationManager(认证管理器),可以在其他地方使用
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* jwt校验过滤器,从http头部Authorization字段读取token并校验
*/
@Bean
public JwtAuthFilter myAuthFilter() throws Exception {
return new JwtAuthFilter();
}
/**
* 认证用户时用户信息加载配置,注入myUserDetailsService
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(mobileCodeAuthenticationProvider)
.authenticationProvider(weChatAuthenticationProvider);
auth
.userDetailsService(myUserDetailsService);
}
/**
* 配置http,包含权限配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 由于使用的是JWT,我们这里不需要csrf
.csrf().disable()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 设置myUnauthorizedHandler处理认证失败、鉴权失败
.exceptionHandling().authenticationEntryPoint(myAuthErrorHandler).accessDeniedHandler(myAuthErrorHandler).and()
// 设置权限
.authorizeRequests()
// 授权不需要登录权限的URL
.antMatchers("/swagger*/**",
"/v2/api-docs",
"/webjars*//**").permitAll()
// 需要登录
.antMatchers("/activity/auth/**").authenticated()
// 需要角色权限
// .antMatchers("/order/test").hasRole("ADMIN")
// hasRole("ROLE_admin") = hasAuthority("admin")
// 除上面外的所有请求全部放开
.anyRequest().permitAll();
// 添加JWT过滤器,JWT过滤器在用户名密码认证过滤器之前
http.addFilterBefore(myAuthFilter(), UsernamePasswordAuthenticationFilter.class);
// 禁用缓存
// http.headers().cacheControl();
}
/**
* 配置跨源访问(CORS)
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}