springboot项目引入security后,security就会创建默认登陆页面,对用户身份权限认证,还会把生成的用户user的密码输出在控制台,只要访问了项目资源就会自动跳转到他的默认登录页面,只有使用他的密码登录后才会去访问我们接口
在前后端分离的项目中,不需要security提供的登陆页面,他的这些配置不符合我们的需求,此时就需要自定义配置,实现我们的需求,比如通过数据库中的账号密码实现登录,而不是通过它提供的账号密码登录。
一. security也提供了这种用户认证的方式。
首先security有一个SecurityContextHolder对象,用于管理security的上下文SeucirtyContext,
SeucirtyContext中存储着Authentication,也就是登录用户的信息
获取SecurityContextHolder对象:
UsernamePasswordAuthenticationToken authentication =
(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
表示通过SecurityContextHolder(上下文管理对象)获取SeucirtyContext(上下文对象)获取Authentication,他里面存储了三个信息,
1、Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象
2、Credentials:用户凭证,一般是密码
3、Authorities:用户权限。
二 .security是通过AuthenticationManager的authenticate完成用户认证。
三.那么AuthenticationManager是如何校验用户信息的呢,
首先我们需要实现一个类实现security中的UserDetialsService接口,只需要实现一个方法
loadUserByUsername,让此方法能根据用户名查询出用户对象,如下:
此方法返回一个Security中的用户数据接口 UserDetails 的实现类对象,所以我么也需要自定义一个实现类
UserDetailsImpl来实现UserDetails接口,用于提供用户基本属性,如账号密码,其他方法一般情况下返回true,如下:
当然此方法也可以重写此接口方法实现其他自定义属性逻辑。这里就不介绍。
此时就完成了在security获取用户信息的基础条件
四. 然后就该实现SecurityConfig的配置类,如下:
@Configuration
@EnableWebSecurity //Security配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired //jwt自定义过滤器
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean //密码编译器,加密密码
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//表示以下路径不需要通过Security验证,可以直接访问
.antMatchers("/api/user/account/login", "/api/user/account/register/","enclosures/updateEnclosures/").permitAll()
//表示以下路径代理到127.0.0.1进行处理
// .antMatchers("/pk/start/game/","/pk/receive/bot/move/").hasIpAddress("127.0.0.1")
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers("/v2/api-docs", "/swagger-resources/configuration/ui",
"/swagger-resources", "/swagger-resources/configuration/security",
"/swagger-ui.html", "/webjars/**","/error").permitAll()
.anyRequest().authenticated();
//把jwt自定义的过滤器添加到Security的链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
五. 自定义认证过滤器
我们需要自定义一个过滤器,实现在访问securityu允许的公共接口外,去验证是否符合权限,也就是去获取请求头中的token是否合法,对token进行解析取出其中的信息,获取对应的登录对象。然后封装Authentication对象存入SecurityContextHolder。如下:
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
//获取前端请求头中的的Authorization的值
String token = request.getHeader("Authorization");
System.out.println(token);
System.out.println(request.getRequestURI());
//判断token是否是"Bearer "开头和token是否为空
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
//如果为token不合法则进行下一个拦截请求,这个不放行
filterChain.doFilter(request, response);
LocalDateTime.now();
return;
}
//token如果合法则除去开头的"Bearer ",留下真正的token
token = token.substring(7);
String userid;
try {
//通过jwt工具类解析token获取token中的userid
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException(e);
}
//通过token中解析出的id从数据库查询user是否存在
User user = userMapper.selectById(Integer.parseInt(userid));
if (user == null) {
throw new RuntimeException("用户名未登录");
}
//UserDetailsImpl是Security自带的封装用户信息等类,
UserDetailsImpl loginUser = new UserDetailsImpl(user);
//通过UsernamePasswordAuthenticationToken类表示从封装的用户类信息的实例中获取用户信息,
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);
//把用户信息存储到Security上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
因为过滤器需要对token进行解析获取用户信息,所以也需要一个jwt工具类,用于生成解析token信息等:如下
@Component
public class JwtUtil {
public static final long JWT_TTL = 60 * 60 * 1000L * 24 ; //token过期时间
public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232131afasdfac"; //生成token密钥
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
//创建返回jwt
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
//构建jwt
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid)
.setSubject(subject)
.setIssuer("sg")
.setIssuedAt(now)
.signWith(signatureAlgorithm, secretKey)
.setExpiration(expDate);
}
//生成和验证jwt的密钥
public static SecretKey generalKey() {
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
}
//解析jwt
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwt)
.getBody();
}
}
六. 登录测试了。
首先是登陆接口。
因为配置类中对登录接口的路径进行了放行,所以不会进入过滤器进行token验证,而是直接执行,
然后service逻辑如下,如果是合法的登录用户,则通过jwt工具类生成含有用户唯一标示信息的token返回给用户,用户需要在访问除了公共接口外的所有接口中带上此token
七:获取信息测试
controller接口为
此接口没有在配置类中放行,所以需要进入过滤器,验证token信息,如果正确就进入service执行相应逻辑,
否则抛出异常,
service逻辑如下,
此时就完成闭环。个人学习总结,如有错误不足,感谢指正