前面介绍了jwt的使用,联合WebMvcConfigurer 并自定义过滤器进行token认证 。现在想使用spring security结合jwt进行认证和授权。
spring security 企业应用级别的安全框架, 核心功能是用户认证Authentication和用户授权Authorization,认证就是登陆验证密码,授权就是不同用户的权限操作问题。
其实也是通过一系列过滤filter进行认证和授权。
说实话,spring security真的内容超多,功能也很强大,我根据自己的理解进行下面的描述。安全框架这里还有Shiro,配置和使用就比较轻松,但功能没有本次的主角强大。对如何选择? 因为spring security需要引入的配置多 比较麻烦, 而spring boot具有自动化配置功能,所以对于spring boot来说,建议使用 spring security。 而ssm 就选择shiro
SSM+Shiro
Spring boot/SpringCloud + Spring security
Spring Security简介
从核心类开始介绍:
- SecurityContextHolder 保存当前用户到上下文中 ,默认模式底层使用threadlocal实现。 提供对SecurityContext的访问
存储 Authentication对象。是 secrity核心类UserDetails的一个实现类。而UserDetails对应数据库中一个用户 在SecurityContextHolder中存放类型的适配器类型。 Authentication对象也可以存放token(string类型的) - SecurityContext 持有Authentication对象在上下文中进行后续过滤 认证,以及授权
- Authentication 就是认证主体了
- GrantedAuthority 认证主体的授权,当前用户权限信息
- UserDetails 构建认证主体 Authentication 属于核心类
- UserDetailsService 通过username 获取UserDetails信息
- UsernamePasswordAuthenticationFilter ,他会封装username ,password 成Authentication 这个声明就叫做
UsernamePasswordAuthenticationToken 也就是索 UsernamePasswordAuthenticationToken实现了 Authentication 接口
接着UsernamePasswordAuthenticationToken会被传递到AuthenticationManager 进行相关的校验认账 - AuthenticationManager 进行验证,其实现类ProviderManager,AuthenticationManager中包含多个AuthenticationProvider
- AuthenticationProvider 验证的核心工作者,也就是做认证工作
AuthenticationProvider 会使用authenticate()方法进行认证,这里manager如何处理provider对应哪个authertication呢, 其实是provider内包含两个方法,一个就是使用authenticate()认证方法,一个就是 boolean supports(Class<?> authertication)方法,后者就是查看验证协议是否合适支持的。
AuthenticationManager 会先调动supports方法,查看是否合适,返回true则可以用该AuthenticationProvider认证。
UsernamePasswordAuthenticationToken 其实对应的是DaoAuthenticationProvider 。
(求你画个图吧,这谁能明白)
从上面的介绍可以大致流程。
Spring security小试牛刀
咱们现在通过代码进行理解:
- mvn引入
<!-- 添加 spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 编写UserInfo类,该类实现UserDetails,前面提到过,userDetails是核心类,后面需要用它构成认证主体 authentication
package domain;
import com.alibaba.fastjson.annotation.JSONField;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* @ClassName UserInfo
* @Description 实现 UserDetails 核心类
* @Author lile
* @Date 2020/2/15 14:43
* @Version 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo implements UserDetails {
private String token; //token
private Integer id;
private String phone;
private List<String> roles; //用户权限 User Admin
private Integer isApp; //登录为app登录 0是,1否
public UserInfo(User user){
this.id = user.getId();
this.phone = user.getPhone();
// 数据库没有建立 Roles规则表,这里统一权限为User
this.roles = Arrays.asList("ROLE_USER");
}
@JSONField(serialize = false)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
- 编写UserInfoService
该service 实现UserDetailsService 编写通过username (此处用唯一的电话号,代替,后续token也使用电话号生成 )获取UserINfo
package com.lile.service;
import com.lile.common.mybits.model.User;
import domain.UserInfo;
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 javax.annotation.Resource;
/**
* @ClassName UserInfoService
* @Description 编写通过username (此处用唯一的电话号,代替,后续token也使用电话号生成 )获取UserINfo
*
*
* @Author lile
* @Date 2020/2/16 12:51
* @Version 1.0
*/
@Service
public class UserInfoService implements UserDetailsService {
@Resource
private UserService userService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userService.findUserByphone(s);
return new UserInfo(user);
}
}
- 编写认证过滤器 JwtAuthenticationTokenFilter,
因为这里是联合 jwt token使用,所以会从requet中获取token,并进行token的验证,验证通过会获取user信息;
接着,该过滤器通过UserInfService 从request 获取UserInfo,构成认证主体authentication 并放置在spring security 上下文中。
package com.lile.handler;
import com.lile.service.UserInfoService;
import domain.UserInfo;
import exceptions.UncheckedException;
import io.jsonwebtoken.JwtException;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import utils.ErrorCode;
import utils.JwtTokenUtil;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;
/**
* @ClassName JwtAuthenticationTokenFilter
* @Description 使用jwt进行 认证token,并获取当前登录用户userInfo
* @Author lile
* @Date 2020/2/16 13:12
* @Version 1.0
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private UserInfoService userInfoService;
@Resource
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("过滤。。。。。");
//获取token
String token = request.getHeader("token");
String phone;
if(token!=null){
try {
phone = jwtTokenUtil.getUsernameFromToken(token);
if(phone!=null&&phone!=""){
UserInfo userInfo = (UserInfo)userInfoService.loadUserByUsername(phone);
// 验证 token
if(jwtTokenUtil.isTokenExpired(token)&& jwtTokenUtil.generateTokenByUsername(phone).equals(token)){
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userInfo, null, userInfo.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
// 全局存放 认证对象
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (JwtException j) {
// throw new UncheckedException(ErrorCode.PERMISSION_DENIED,"token错误,权限不足");
}
}
filterChain.doFilter(request,response);
}
/// 后面重构上面的俄罗斯套娃
private UserInfo getUserInfobyT(String token){
String phone ="";
phone = jwtTokenUtil.getUsernameFromToken(token);
UserInfo userInfo = (UserInfo) userInfoService.loadUserByUsername(phone);
return userInfo;
}
}
- 构建安全配置类 WebSecurityConfig , 继承WebSecurityConfigurerAdapter 。重写方法,定义相关接口url所需要的权限,并添加上面的过滤器
package com.lile.handler;
import com.lile.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
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.builders.WebSecurity;
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.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsUtils;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName WebSecurityConfig
* @Description 安全配置类
* @Author lile
* @Date 2020/2/16 16:34
* @Version 1.0
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
// Spring会自动寻找同样类型的具体类注入,这里就是JwtUserDetailsServiceImpl了
@Resource
private UserInfoService userInfoService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userInfoService).passwordEncoder(passwordEncoderBean());
}
@Bean
public PasswordEncoder passwordEncoderBean() {
return new BCryptPasswordEncoder();
}
// @Autowired
// public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
// authenticationManagerBuilder
// // 设置UserDetailsService
// .userDetailsService(this.userInfoService)
// // 使用BCrypt进行密码的hash
// .passwordEncoder(passwordEncoder());
// }
// // 装载BCrypt密码编码器
// @Bean
// public PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder();
// }
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
System.out.println("安全配置。。");
httpSecurity
//禁用CSRF保护
.csrf().disable()
.authorizeRequests()
// 放行option请求
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
// 对于获取token的rest api要允许匿名访问
.antMatchers("/hello").permitAll()
.mvcMatchers("/v2/**").permitAll()
//.mvcMatchers("/users/**").permitAll()
.mvcMatchers("/hello/**").permitAll()
.mvcMatchers("/swagger-resources/**").permitAll()
.mvcMatchers("/swagger-ui.html#!/**").permitAll()
//跨域 post请求两次,第一次预检请求,method 为 OPTIONS 。
.antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
//任何访问都必须授权
.anyRequest().authenticated()
//配置那些路径可以不用权限访问
.and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.formLogin()
//登陆成功后的处理,因为是API的形式所以不用跳转页面
.successHandler(new RestAuthenticationSuccessHandler())
//登陆失败后的处理
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
.and()
//登出后的处理
.logout().logoutSuccessHandler(new RestLogoutSuccessHandler())
.and()
//认证不通过后的处理
.exceptionHandling()
.authenticationEntryPoint(new RestAuthenticationEntryPoint());
// 禁用缓存
httpSecurity.headers().cacheControl();
// 添加Filter 如果不添加 会禁止Filter执行 使用filter过滤token
httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() {
return new JwtAuthenticationTokenFilter();
}
/**
* 登陆成功后的处理
*/
public static class RestAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) {
clearAuthenticationAttributes(request);
}
}
/**
* 登出成功后的处理
*/
public static class RestLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) {
}
}
/**
* 权限不通过的处理
*/
public static class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
//返回json形式的错误信息
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
//设置允许跨域的配置
// 这里填写你允许进行跨域的主机ip(正式上线时可以动态配置具体允许的域名和IP)
response.setHeader("Access-Control-Allow-Origin", "*");
// 允许的访问方法
response.setHeader("Access-Control-Allow-Methods", "*");
// Access-Control-Max-Age 用于 CORS 相关配置的缓存
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "*");
response.getWriter().println("{\"code\": -2,\"msg\": \"登陆状态已过期\"}");
response.getWriter().flush();
}
}
}
配置完成 ,使用:@PreAuthorize("hasRole('User')") 表示user权限
@Api(value = "/user", description = "用户")
@RequestMapping("users")
@RestController
@Slf4j
@PreAuthorize("hasRole('USER')")
public class UserController {
@Resource
private UserService userService;
- 启动项目, 控制台生成用户密码:
每次启动都不一样,用户名默认 user
自定义用户名密码:
配置文件
#security
spring.security.user.name=lile
spring.security.user.password=shuaige
打印security日志的配置
#logging
logging.level.org.springframework.data:DEBUG
logging.level.org.springframework.security:DEBUG
- 使用wagger 试验一下:
用上面的用户名密码登录
这里登录后,发现swagger空白页面,搞什么鬼, 这个时候不能掉链子, f12后发现 swagger resource 404,后面查找发现,自己使用了WebMvcConfigurer添加了 过滤器,过滤器应该设置不包括 swagger相关url:
@Override
public void addInterceptors(InterceptorRegistry registry) { // 添加拦截器
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/user/login")
.excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
}
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/");
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
再使用 postman尝试一下: /hello 接口
因为configure 那里规则为
.antMatchers("/hello").permitAll()
所以 /hello接口可以无权限访问。
试一下 /user/1 接口 (获取id 为1 的用户信息)
显示登出,因为该接口请求没有token信息 ,上面对权限不够进行了一次封装:
RestAuthenticationEntryPoint
看下后台
- 使用token, 这里需要在登录时候 返回token 给前端,前端请求时候,请求头添加 authorization:token
// 处理token 封装res
String token = jwtTokenUtil.generateTokenByUsername(loginRequest.getPhone());
UserInfo userInfo = new UserInfo(user);
userInfo.setToken(token);
return userInfo;