我的项目是用SpringBoot 搭建的一个App-Server,用来响应移动端的访问请求,设计的方式是前后端分离的 。本来对权限的做法是在请求里面加上token 字段,然后服务器端再对token做解析,得到userid,再根据userid 查找数据库,来判断当前用户是否有权限访问这个接口。token 是用的JWT;这样做除了每个接口都要写解析token 和 权限的判断代码外,感觉也没有其他问题。
哪位有经验的兄弟能解答一下这样做有什么不妥的地方吗?不甚感激。测试接口和服务端代码如下:
@PostMapping("/test")
@ResponseBody
public BaseResponse test(@RequestBody BaseQueryDataReq req) {
BaseResponse rsp = new BaseResponse();
if(req.getToken() == null || req.getToken().isEmpty())
return ResponseFactory.getErrorResponse(ErrorEnum.ERROR_TOKEN_ERROR);
else if(JwtHelper.isTokenExpiration(req.getToken()))
return ResponseFactory.getErrorResponse(ErrorEnum.ERROR_TOKEN_EXPIRE);
String userid = JwtHelper.getUserIdFromToken(req.getToken());
if(userid == null || userid.isEmpty() || !req.getUserid().equals(userid))
return ResponseFactory.getErrorResponse(ErrorEnum.ERROR_TOKEN_ERROR);
...
...
}
这个方法是参考各大开发者平台的接口定义来的;他们的接口中,大部分都需要在json里面加上一个token字段。但看了Spring Security的一些文章后,觉得不用这个就感觉不正宗一样,所以我也尝试着研究Spring Security。
一般来讲,对于RESTful API都会有认证(Authentication)和授权(Authorization)过程,保证API的安全性。
Authentication vs. Authorization
Authentication指的是确定这个用户的身份(用户账号),Authorization是确定该用户拥有什么操作权限,(用户角色role)。
认证方式一般有三种
Basic Authentication
这种方式是直接将用户名和密码放到Header中,使用Authorization: Basic Zm9vOmJhcg==
,使用最简单但是最不安全。
这种方式也是再HTTP头中,使用Authorization: Bearer
,使用最广泛的TOKEN是JWT,通过签名过的TOKEN。
这种方式安全等级最高,但是也是最复杂的。如果不是大型API平台或者需要给第三方APP使用的,没必要整这么复杂。
一般项目中的RESTful API使用JWT来做认证就足够了。
实现方式大致可以分为这几种:
1.配置文件实现,只需要在配置文件中指定拦截的url所需要权限、配置userDetailsService指定用户名、密码、对应权限,就可以实现。
2.实现UserDetailsService,loadUserByUsername(String userName)方法,根据userName来实现自己的业务逻辑返回UserDetails的实现类,需要自定义User类实现UserDetails,比较重要的方法是getAuthorities(),用来返回该用户所拥有的权限。
3.通过自定义filter重写spring security拦截器,实现动态过滤用户权限。
4.通过自定义filter重写spring security拦截器,实现自定义参数来检验用户,并且过滤权限。
我要讲的就是第二种方式,用自定义的User类来实现UserDetails,UserDetails的源码如下, 从源码可以看出,我们需要重点实现的是获取用户权限,用户名,用户密码这三个接口;简单实现见JwtUserDetails;
public interface UserDetails extends Serializable {
// ~ Methods
// ========================================================================================================
/**
* Returns the authorities granted to the user. Cannot return null
.
*
* @return the authorities, sorted by natural key (never null
)
*/
Collection extends GrantedAuthority> getAuthorities();
/**
* Returns the password used to authenticate the user.
*
* @return the password
*/
String getPassword();
/**
* Returns the username used to authenticate the user. Cannot return null
.
*
* @return the username (never null
)
*/
String getUsername();
/**
* Indicates whether the user's account has expired. An expired account cannot be
* authenticated.
*
* @return true
if the user's account is valid (ie non-expired),
* false
if no longer valid (ie expired)
*/
boolean isAccountNonExpired();
/**
* Indicates whether the user is locked or unlocked. A locked user cannot be
* authenticated.
*
* @return true
if the user is not locked, false
otherwise
*/
boolean isAccountNonLocked();
/**
* Indicates whether the user's credentials (password) has expired. Expired
* credentials prevent authentication.
*
* @return true
if the user's credentials are valid (ie non-expired),
* false
if no longer valid (ie expired)
*/
boolean isCredentialsNonExpired();
/**
* Indicates whether the user is enabled or disabled. A disabled user cannot be
* authenticated.
*
* @return true
if the user is enabled, false
otherwise
*/
boolean isEnabled();
}
public class JwtUserDetails implements UserDetails {
private String userName;
private String password;
private Collection extends GrantedAuthority> 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;
}
}
Spring Security中进行身份验证的是AuthenticationManager接口,ProviderManager是它的一个默认实现,但它并不用来处理身份认证,而是委托给配置好的AuthenticationProvider,每个AuthenticationProvider会轮流检查身份认证。检查后或者返回Authentication对象或者抛出异常。
验证身份就是加载相应的UserDetails,看看是否和用户输入的账号、密码、权限等信息匹配。此步骤由实现AuthenticationProvider的DaoAuthenticationProvider(它利用UserDetailsService验证用户名、密码和授权)处理。包含 GrantedAuthority 的 UserDetails对象在构建 Authentication对象时填入数据。
最终我需要实现的就是:
1,在login 接口中 返回JWT 的token,其中携带username 信息;
2,在Spring Security框架中,自定义一个filter ,在 UserNamePasswordFilter 之前进行验证,验证通过后,写入SpringSecurityContext;
3,除获取token 之外的接口(注册,登录)其他接口需要携带token 访问;
配置文件如下:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtUserDetailService jwtUserDetailService;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
// 设置UserDetailsService
.userDetailsService(jwtUserDetailService)
// 使用BCrypt进行密码的hash
.passwordEncoder(passwordEncoder());
}
// 装载BCrypt密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtUserDetailService);
}
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/auth").authenticated() // 需携带有效 token
.antMatchers("/admin").hasAuthority("admin") // 需拥有 admin 这个权限
.antMatchers("/ADMIN").hasRole("ADMIN") // 需拥有 ADMIN 这个身份
.antMatchers("/register").permitAll()
.antMatchers("/login").permitAll()
.anyRequest().authenticated() // 允许所有请求通过
.and()
.csrf()
.disable() // 禁用 Spring Security 自带的跨域处理
.sessionManagement() // 定制我们自己的 session 策略
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 调整为让 Spring Security 不创建和使用 session
httpSecurity.addFilterBefore(authenticationTokenFilterBean(),JwtTokenFilter.class);
}
@Bean
public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
JwtTokenFilter authenticationTokenFilter = new JwtTokenFilter(authenticationManagerBean());
authenticationTokenFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationTokenFilter;
}
filter 如下:
@Component
public class JwtTokenFilter extends UsernamePasswordAuthenticationFilter {
/**
* json web token 在请求头的名字
*/
@Value("${token.header}")
private String tokenHeader;
/**
* 辅助操作 token 的工具类
*/
@Autowired
private JwtTokenUtils tokenUtils;
@Autowired
private JwtUserDetailService userDetailsService;
@Autowired
private JwtTokenUtils jwtTokenUtil;
public JwtTokenFilter(AuthenticationManager authenticationManager) {
setAuthenticationManager(authenticationManager);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 将 ServletRequest 转换为 HttpServletRequest 才能拿到请求头中的 token
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 尝试获取请求头的 token
String authToken = httpRequest.getHeader(this.tokenHeader);
System.out.println("getHeader(\"Authorization\")" + httpRequest.getHeader("Authorization"));
// 尝试拿 token 中的 username
// 若是没有 token 或者拿 username 时出现异常,那么 username 为 null
String username = this.tokenUtils.getUsernameFromToken(authToken);
// 如果上面解析 token 成功并且拿到了 username 并且本次会话的权限还未被写入
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (this.tokenUtils.validateToken(authToken, userDetails)) {
// 生成通过认证
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
// 将权限写入本次会话
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
API 接口如下: login ; register 接口不需要token可以访问; query_test 接口需要在header 里携带token
@RestController
public class MainControler {
@Autowired
UserRepository userRepository;
@Autowired
JwtTokenUtils tokenUtils;
@PostMapping("/login")
@ResponseBody
public BaseReq login(@RequestBody AppUser req)
{
AppUser u = userRepository.findByUsername(req.getUsername());
BaseReq rsp = new BaseReq();
String token = tokenUtils.generateToken(req.getUsername());
rsp.setToken(token);
return rsp;
}
@PostMapping("/register")
@ResponseBody
public AppUser register(@RequestBody AppUser req)
{
AppUser u = userRepository.findByUsername(req.getUsername());
if(u == null)
u = userRepository.save(req);
else
u = userRepository.save(u);
String token = tokenUtils.generateToken(req.getUsername());
System.out.println("token : "+ token);
return u;
}
@PostMapping("/query_test")
@ResponseBody
public AppUser query(@RequestBody AppUser req)
{
AppUser u = userRepository.findByUsername(req.getUsername());
return u;
}
不带token 访问时,提示403 ,无权限;携带token 后,能正常访问。
https://blog.csdn.net/my_learning_road/article/details/79833802
https://www.xncoding.com/2017/07/09/spring/sb-jwt.html