本文以springboot+spring securit +jwt 实现前后端分离的场景,登录之后返回token完成之后的鉴权
1.入门知识
spring-security
├── 核心 - spring-security-core.jar
├── Remoting - spring-security-remoting.jar
├── Web - spring-security-web.jar
├── 配置 - spring-security-config.jar
├── LDAP - spring-security-ldap.jar
├── OAuth 2.0核心 - spring-security-oauth2-core.jar
├── OAuth 2.0客户端 - spring-security-oauth2-client.jar
├── OAuth 2.0 JOSE - spring-security-oauth2-jose.jar
├── ACL - spring-security-acl.jar
├── CAS - spring-security-cas.jar
├── OpenID - spring-security-openid.jar
└── 测试 - spring-security-test.jar
[
](https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/)
2.配置
1.核心配置类
@EnableWebSecurity 开启Spring Security的功能
需要继承WebSecurityConfigurerAdapter
@Configuration
@EnableWebSecurity // 开启Spring Security的功能
//prePostEnabled = true表明开启 @PreAuthorize(方法之前验证授权),@PostAuthorize(授权方法之后被执行),@PostFilter(在方法执行之后执行,用于过滤集合),@PreFilter(在方法执行之前执行,用于过滤集合);功能强大,所以其使用需要借助spring的EL表达式;
//securedEnabled = true,表明使用 @Secured 注解 开启权限
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("myUserDetailService")
private UserDetailsService userDetailsService;
@Autowired
private UnauthorizedEntryPoint unauthorizedEntryPoint;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
//放行名单
private final static String[] WHITE_URL_LIST = {
"/swagger-ui.html/**",
"/api/**",
"/resources/**",
"/auth/**",
"/doc.html*",
"/v2/api-docs",
"/swagger-resources/**",
"/static/**",
"/**/*.js",
"/**/*.html",
"/**/*.css",
"/*.txt",
"/login/**",
"/auth/**",
};
/**
* 核心配置
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("我进来了");
http.csrf().disable().cors()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //session创建策略,永不
.and()
.httpBasic()
// 未经过认证的用户访问受保护的资源
.authenticationEntryPoint(unauthorizedEntryPoint)
.and()
.authorizeRequests()
.antMatchers(WHITE_URL_LIST).permitAll() //auth服务放行名单
.anyRequest().authenticated()
.and()
.formLogin().disable()
.logout().logoutSuccessHandler((httpServletRequest, httpServletResponse, authentication) ->
ResponseUtils.responseReturn(httpServletResponse, Result.ok("注销成功")))
.permitAll();
//异常处理
http.exceptionHandling()
// 已经认证的用户访问自己没有权限的资源处理
.accessDeniedHandler((httpServletRequest, httpServletResponse, e) ->
ResponseUtils.responseReturn(httpServletResponse, Result.error(ResultEnum.FORBIDDEN.getCode(),ResultEnum.FORBIDDEN.getMsg())))
.and().addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry
= http.authorizeRequests();
}
/**
* 认证管理器配置
*/
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码处理
*
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
/**
* 装载BCrypt密码编码器,官方推荐的,
* 是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10.
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
其中有三个组件
a.UserDetailsService
通过实现该接口完成自定义的身份认证,若不配置则有 spring security 自定义生成;
/**
* Security身份认证之UserDetailsService
*
*/
@Service("myUserDetailService")
public class MyUserDetailService implements UserDetailsService {
@Resource
private UserService userService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
String regPattern = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$";
Pattern pattern = Pattern.compile(regPattern);
Matcher matcher = pattern.matcher(userName);
boolean isMatch = matcher.matches();
//若是手机号
if (isMatch){
final com.fufu.idea.entity.user.User user = userService.selectByPhone(userName);
//TODO 后期需要查询出权限列表?或者其他放是实现后台权限
if (ObjectUtils.isEmpty(user)){
throw new UsernameNotFoundException(String.format("账号'%s'不存在", userName));
}
// TODO 查该用户拥有的角色,来分配具体的访问权限(或者没有手机号登录的账号不返回token直接放开可以浏览的接口)
return new User(userName, BCrypt.hashpw(userName, BCrypt.gensalt()),new ArrayList<>());
}else {
throw new UsernameNotFoundException(String.format("账号'%s'不存在", userName));
}
}
}
该接口只有一个 loadUserByUsername() 方法,返回UserDetails类型,实现中需要具体返回org.springframework.security.core.userdetails.User类,不要和项目中的User类搞混;
构造函数中的三个参数:
username:用户名
password: 密码
authorities: 权限集合
若自定义的用户名没有查到具体的账号信息需要抛出 UsernameNotFoundException 这样可以使用通用配置后的统一异常返回;这里建议用户名查找设置为系统里用户的唯一值来进行查找,例如手机号或者登录账号;
b.AuthenticationEntryPoint
当用户请求了一个受保护的资源,但是用户认证没有通过,那么抛出异常就会被 ExceptionTranslationFilter 捕获,之后会调用 AuthenticationEntryPoint.Commence()方法;
security异常统一处理方式,实现AuthenticationEntryPoint接口
/**
* <p>
* security异常统一处理方式
* </p>
*
* @author chenziyuan
*
*/
@Component
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException,
ServletException {
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
if (e instanceof BadCredentialsException) {
ResponseUtils.responseReturn(httpServletResponse, Result.error(ResultEnum.LOGIN_ERROR.getCode(), ResultEnum.LOGIN_ERROR.getMsg()));
} else {
ResponseUtils.responseReturn(httpServletResponse, Result.error(ResultEnum.FORBIDDEN.getCode(), ResultEnum.FORBIDDEN.getMsg()));
}
}
}
c.OncePerRequestFilter
OncePerRequestFilter是Spring Boot里面的一个过滤器抽象类,其同样在Spring Security里面被广泛用到,通常用到只执行一次的请求;
重写 doFilterInternal 方法:
主要是解析token里的信息是否正确
/**
* @Author: chenziyuan.
* @Description: 登录验证令牌解析过滤器
* @Date: 2020/4/9
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private UserDetailsService userDetailsService;
@Autowired
public JwtAuthenticationTokenFilter(@Qualifier("myUserDetailService") UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(SysConstant.TOKEN_HEADER);
/* if(SpringContextUtil.getBean(Environment.class).getActiveProfiles()[0].equals(ProfileConstant.PROFILE_DEV)){
//测试环境下免登录
UserDetails userDetails = userDetailsService.loadUserByUsername(ProfileConstant.PROFILE_DEV_USER);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
UserInfo userInfo = new UserInfo();
userInfo.setAccount(ProfileConstant.PROFILE_DEV_USER);
userInfo.setUserId(1);
userInfo.setRoleId(1);
userInfo.setPassword("123456");
response.setHeader(SysConstant.TOKEN_HEADER, JwtHelper.generateToken(userInfo));
}else{*/
//&& authHeader.startsWith(TOKEN_PREFIX)
if (authHeader != null && !"null".equals(authHeader)) {
String authToken = authHeader;
//令牌登录的用户信息
String userName = JwtUtils.getUsernameFromToken(authToken);
if (StringUtils.isEmpty(userName)) {
ResponseUtils.responseReturn(response, 401, Result.error(401, "token无效"));
}
//获取过期时间
if (JwtUtils.isTokenExpired(authToken)) {
ResponseUtils.responseReturn(response, 401, Result.error(401, "token过期"));
}
//获取生成的自定义token信息
//获取userDetail
if (SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userName);
if (JwtUtils.validateToken(authToken)) {
//判断令牌是否有效
if (validateToken(userName,userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
//TODO 将来若要存缓存则记得将缓存中的token删除
ResponseUtils.responseReturn(response, 401, Result.error(401, "token无效"));
}
}
}
}
filterChain.doFilter(request, response);
}
/**
* 校验令牌信息
*
* @param userName 登录用户名
* @param
* @param userDetails 用户信息
* @return boolean
*/
private boolean validateToken(String userName, UserDetails userDetails) {
String regPattern = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$";
Pattern pattern = Pattern.compile(regPattern);
Matcher matcher = pattern.matcher(userDetails.getUsername());
boolean isMatch = matcher.matches();
//若是手机号
if (isMatch) {
return userDetails.getUsername().equals(userName);
}
return false;
}
}
3. 登录入口处理
登录接口要生成token,并将用户信息spring security中的上下文供以后验证用
@ApiOperation("登录")
@PostMapping()
public Result login(@Validated @RequestBody UserLoginDTO dto){
//获取spring security 上下文
final SecurityContext context = SecurityContextHolder.getContext();
//生成token
final String token = JwtUtils.generateToken(dto.getPhone());
final UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(dto.getPhone(),dto.getPhone());
context.setAuthentication(usernamePasswordAuthenticationToken);
return Result.ok("登录成功",token);
}