社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
该系列文章,是我从零搭建一套后端项目的经验总结。
没有过多的流程梳理和细节描写,目的也是对所学技术的一个总结和巩固。
内容大多包括实现思路,实现要点、注意事项和一些个人心得,希望对各位看官有所帮助。
转载请注明来源
注意:
若没有走security的UsernamePasswordAuthenticationToken、UserDetailsService的那一套验证。
要重写 AuthenticationProvider 的 authenticate(验证)方法,自定义security的权限验证。
否则会报错 Bad credentials
1.依赖注入:
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
2.TokenAuthentication类:有创建Token和解析Token的两个方法以及相关常量的定义
public class TokenAuthentication {
private static final long EXPIRATION_TIME = 86_400_000; // 1天
private static final String SECRET = "P@Xue"; // JWT密码
private static final String TOKEN_PREFIX = "Bearer"; // Token前缀
private static final String HEADER_STRING = "Authorization";// 存放Token的Header Key
/*根据jwt创建token*/
public static String creatTokenByJwt(String userInfo) {
String[] userInfoA = userInfo.split(",");
return Jwts.builder()
.claim("userId", userInfoA[0])
.claim("userName", userInfoA[1])
.claim("address", userInfoA[2])
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
/*解析token*/
public Authentication parseToken(HttpServletRequest request, HttpServletResponse response) {
String token = request.getHeader(HEADER_STRING);
if (Tools.notEmpty(token)) {
// 解析 Token
Claims claims = Jwts.parser()
// 验签
.setSigningKey(SECRET)
// 去掉 Bearer
.parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody();
String userId = claims.get("userId").toString();
//具备访问权限 重新生成jwt
if (checkUrl(userId, request)) {
String userInfo = claims.get("userId").toString() + "," + claims.get("userName").toString() + "," + claims.get("address").toString();
token = creatTokenByJwt(userInfo);
response.setHeader(HEADER_STRING, token);
return new UsernamePasswordAuthenticationToken(userInfo,null);
}
return null;
}
return null;
}
/**
* 根据userId从数据库中取出该用户对应的API权限,然后与request中的请求API做比较,检验是否具备访问权限
* @param userId
* @param request
* @return
*/
private boolean checkUrl(String userId, HttpServletRequest request) {
return "1".equals(userId);
}
}
3.登录接口类,验证用户名和密码。通过后调用自定义的authenticate()方法。然后userInfo信息生成token
@Override
public Result login(UserPO userPO, HttpServletRequest request) {
//根据用户名查询出来用户信息
UserPO msg = userMP.login(userPO);
if (msg == null) {
return Result.error("用户不存在");
}
if (!Md5Utils.hash(userPO.getPassword()).equals(msg.getPassword())) {
return Result.error("密码错误");
}
String userInfo = msg.getUserId() + "," + msg.getUserName() + "," + msg.getAddress();
//使用自定义的authenticate验证
Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userInfo,null));
//根据jwt和用户信息生成token
String token = TokenAuthentication.creatTokenByJwt(authentication.getName());
return Result.ok("登陆成功").put("token",token);
}
4.自定义的AuthenticationProvider实现类authenticate。
这里开始对security的理解不够透彻,一直报错 Bad credentials,找不到原因。 网上的解决方案,大多是用户名、密码错误之类的。 以这个错误为出发点,查阅了相关资料,了解了其原理和源码,重写authenticate方法解决问题。
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取认证的用户名 & 密码
Authentication auth = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(),
authentication.getCredentials());
SecurityContextHolder.getContext().setAuthentication(authentication);
return auth;
}
// 是否可以提供输入类型的认证服务
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
5.重写WebSecurityConfigurerAdapter类的configure方法
package com.example.frame.config;
import com.example.frame.common.TokenAuthentication;
import com.example.frame.common.filter.JWTAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* security的路由配置
*
* @Author: Jie Li
* @CreateTime: 2019/9/10.
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
JWTAuthenticationFilter jWTAuthenticationFilter() {
return new JWTAuthenticationFilter();
}
@Bean
TokenAuthentication tokenAuthenticationSV() {
return new TokenAuthentication();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf验证
http.csrf().disable()
// 对请求进行认证
.authorizeRequests()
//所有的跨域请求全部打开
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/inner/**").permitAll()
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources").permitAll()
.antMatchers("/beans").permitAll()//显示所有的spring bean
.antMatchers("/autoconfig").permitAll()//显示自动配置信息
.antMatchers("/configprops").permitAll()//显示配置属性列表
.antMatchers("/dump").permitAll()//显示线程活动的快照
.antMatchers("/env").permitAll()//显示应用程序的环境变量
.antMatchers("/health").permitAll()//显示应用程序的健康指标
.antMatchers("/info").permitAll()//显示应用的信息
.antMatchers("/mappings").permitAll()//显示所有的URL路径
.antMatchers("/metrics").permitAll()//显示应用的度量标准信息
.antMatchers("/trace").permitAll()//显示跟踪信息
.antMatchers("/druid/**").permitAll()//显示连接池
// 所有 /login 的POST请求 都放行
.antMatchers(HttpMethod.POST, "/login").permitAll()
.antMatchers(HttpMethod.GET, "/v2/api-docs").permitAll()
// 所有请求需要身份认证
.anyRequest().authenticated().and()
// 添加一个过滤器验证其他请求的Token是否合法
.addFilterBefore(jWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
/**
* 忽略静态资源
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/webjars/**")
.antMatchers("/swagger-resources/**")
.antMatchers("/file/**");
// 可以仿照上面一句忽略静态资源
}
}
注意:
要将自己定义的过滤器和Token管理类注入到bean容器中。
// 添加一个过滤器验证其他请求的Token是否合法
.addFilterBefore(jWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
添加自定义的过滤器进行校验。
6.自定义过滤器类:JWTAuthenticationFilter,重写doFilter方法:
public class JWTAuthenticationFilter extends GenericFilterBean {
@Autowired
TokenAuthentication tokenAuthentication;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
Authentication authentication = tokenAuthentication.parseToken((HttpServletRequest) request, (HttpServletResponse) response);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
注意
:
1.先调用解析Token方法parseToken(),鉴权通过后。调用SecurityContextHolder.getContext().setAuthentication(authentication);
将权限信息(会包括一些基本的用户信息)放入SecurityContext管理的authentication中。
可以通过 **SecurityContextHolder.getContext().getAuthentication();**获取到权限信息。
方法有以下几种:
2.Token解析,自己的逻辑校验完成后,会执行
filterChain.doFilter(request, response);
也就是执行下一个过滤器,按照security默认的过滤器链执行。
鉴权成功,执行完过滤器之后,就会重定向到我们的请求接口上,执行我们具体的业务请求:
@RestController
@RequestMapping("/api/user")
public class UserCL {
private final UserSV userSV;
@Autowired
public UserCL(UserSV userSV) {
this.userSV = userSV;
}
@GetMapping("/getName")
public UserPO getName(String id) {
return userSV.getName(id);
}
}
解决方案:
1.若用了 UsernamePasswordAuthenticationToken、UserDetailsService 去进行校验,则去检查 用户名不存在,密码错误等情况。
2.若没有使用 UsernamePasswordAuthenticationToken 去校验,自定义了过滤器去校验,则必须要重写 AuthenticationProvider 的 authenticate(验证)方法。上文中有提及。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!