Spring Security+JWT登录的三种方式
1、访问接口/demo01
- 当用户从浏览器发送请求访问/demo01接口时,服务端会返回302响应码,让客户端重定向到/login页面,用户在/login页面登录,登陆成功之后,就会自动跳转到/demo01接口。
- 默认的账户是admin,密码会在项目启动的时候在控制台自动生成。
2、在配置文件中自定义账号和密码
- 配置完成后重启项目就可以使用自定义的账号密码登录了
server:
port: 8888
spring:
security:
user:
name: admin
password: admin
Java 配置用户名/密码
- 自定义配置类继承WebSecurityConfigurerAdapter,实现config方法。(注意:要是不添加roles启动会报错——Cannot pass a null GrantedAuthority collection)
- Spring Security中提供了 BCryptPasswordEncoder 密码编码工具,可以非常方便的实现密码的加密加盐,相同明文加密出来的结果总是不同,这样就不需要用户去额外保存盐的字段了
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//下面的配置表示在内存中配置用户
auth.inMemoryAuthentication()
.withUser("jack")
.password(passwordEncoder().encode("123"))
.roles("admin");
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3、详细登录配置以及配置类中的所有方法以及实体
- 对于登录接口,登录成功后的响应,登录失败后的响应,我们都可以在 WebSecurityConfigurerAdapter 的实现类中进行配置。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomAuthenticationProvider authenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
expressionUrlAuthorizationConfigurer.expressionInterceptUrlRegistry registry = http
.authorizeRequests();
// 白名单方法(所有放行的接口)
for (String url : ignoreUrlsConfig().getUrls()) {
registry.antMatchers(url).permitAll();
}
registry
// 放行所有OPTIONS请求
.antMatchers(HttpMethod.OPTIONS).permitAll()
// 其他请求都需要认证后才能访问
.anyRequest().authenticated()
// 使用自定义的 accessDecisionManager
.accessDecisionManager(accessDecisionManager())
.and()
// 添加未登录与权限不足异常处理器
.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler())
.authenticationEntryPoint(restAuthenticationEntryPoint())
.and()
// 将自定义的JWT过滤器放到过滤链中
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
// 打开Spring Security的跨域
.cors()
.and()
// 关闭CSRF
.csrf().disable()
// 关闭Session机制
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {
return new RestfulAccessDeniedHandler();
}
@Bean
public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
return new RestAuthenticationEntryPoint();
}
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public AccessDecisionVoter accessDecisionProcessor() {
return new AccessDecisionProcessor();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
// 构造一个新的AccessDecisionManager 放入两个投票器
List> decisionVoters = Arrays.asList(new WebexpressionVoter(), accessDecisionProcessor());
return new Unanimousbased(decisionVoters);
}
@Bean
public IgnoreUrlsConfig ignoreUrlsConfig() {
return new IgnoreUrlsConfig();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
}
AccessDecisionProcessor
public class AccessDecisionProcessor implements AccessDecisionVoter {
@Override
public int vote(Authentication authentication, FilterInvocation object, Collection attributes) {
assert authentication != null;
assert object != null;
// 拿到当前请求uri
String requestUrl = object.getRequestUrl();
String method = object.getRequest().getMethod();
log.debug("进入自定义鉴权投票器,URI : {} {}", method, requestUrl);
String key = requestUrl + ":" + method;
return ACCESS_GRANTED;
// 如果没有缓存中没有此权限也就是未保护此API,弃权
PermissionInfoBO permission = caffeineCache.get(CacheName.PERMISSION, key, PermissionInfoBO.class);
if (permission == null) {
return ACCESS_ABSTAIN;
}
// 拿到当前用户所具有的权限
List roles = ((UserDetail) authentication.getPrincipal()).getRoles();
if (roles.contains(permission.getRoleCode())) {
return ACCESS_GRANTED;
} else {
return ACCESS_DENIED;
}
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class> clazz) {
return true;
}
}
RestfulAccessDeniedHandler
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e) throws IOException, ServletException {
response.setHeader("Cache-Control", "no-cache");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().println(JSONObject.toJSONString(StandardResultVO.forbiddenResult()));
response.getWriter().flush();
}
}
RestAuthenticationEntryPoint
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setHeader("Cache-Control", "no-cache");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().println(JSONObject.toJSONString(StandardResultVO.errorResult(401, authException.getMessage())));
response.getWriter().flush();
}
}
JwtAuthenticationTokenFilter
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtProvider jwtProvider;
@Autowired
private JwtProperties jwtProperties;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 拿到Authorization请求头内的信息
String authToken = jwtProvider.getToken(request);
// 判断一下内容是否为空
if (StrUtil.isNotEmpty(authToken) && authToken.startsWith(jwtProperties.getTokenPrefix())) {
// 去掉token前缀,拿到真实token
authToken = authToken.substring(jwtProperties.getTokenPrefix().length());
// 拿到token里面的登录账号
String loginAccount = jwtProvider.getSubjectFromToken(authToken);
if (StrUtil.isNotEmpty(loginAccount) && SecurityContextHolder.getContext().getAuthentication() == null) {
// 查询用户
UserDetails userDetail = userDetailsService.loadUserByUsername(loginAccount);
// 拿到用户信息后验证用户信息与token
if (userDetail != null && jwtProvider.validateToken(authToken, userDetail)) {
// 组装authentication对象,构造参数是Principal Credentials 与 Authorities
// 后面的拦截器里面会用到 grantedAuthorities 方法
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetail, userDetail.getPassword(), userDetail.getAuthorities());
// 将authentication信息放入到上下文对象中
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("JWT过滤器通过校验请求头token自动登录成功, user : {}", userDetail.getUsername());
}
}
}
chain.doFilter(request, response);
}
}
IgnoreUrlsConfig
@Getter
@Setter
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {
private List urls = new ArrayList<>();
}
CustomAuthenticationProvider
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String passwd = (String) authentication.getCredentials();
UserDetails userDetails = userDetailsService.loadUserByUsername(name);
if (Objects.isNull(userDetails) || StrUtil.isEmpty(userDetails.getUsername())) {
throw new BadCredentialsException("用户不存在");
}
boolean verify = MD5Util.verify(passwd, userDetails.getPassword());
if (verify) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities());
result.setDetails(authentication.getDetails());
return result;
}
throw new BizException("用户名或密码错误,请重新输入。");
}
@Override
public boolean supports(Class> aClass) {
return true;
}
}
CustomUserDetailsService
@Slf4j
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private AccountService accountService;
@Autowired
private UserRoleService userRoleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.debug("开始登陆验证,用户名为: {}", username);
// 根据用户名验证用户
UserInfo userInfo = null;
if (RegexUtil.isMobile(username)) {
log.debug("手机号登陆:{}", username);
userInfo = accountService.getUserInfoByPhone(username);
} else {
log.debug("用户名或者账号登陆:{}", userInfo);
userInfo = accountService.getUserInfoByNameOrAccount(username);
}
if (userInfo == null) {
throw new BizException("用户名或密码错误,请重新输入。");
}
// 构建UserDetail对象
UserDetail userDetail = new UserDetail();
userDetail.setUserInfo(userInfo);
List roles = userRoleService.getRolesByUserId(userInfo.getUserId());
userDetail.setRoleInfoList(roles);
return userDetail;
}
}
JwtProvider
@Slf4j
@Component
public class JwtProvider {
@Autowired
private JwtProperties jwtProperties;
public String getToken(HttpServletRequest request) {
return request.getHeader(jwtProperties.getRequestHeader());
}
public AccessToken createToken(UserDetails userDetails) {
return createToken(userDetails.getUsername());
}
public AccessToken createToken(String subject) {
// 当前时间
final Date now = new Date();
// 过期时间
final Date expirationDate = new Date(now.getTime() + jwtProperties.getExpirationTime() * 1000);
String token = jwtProperties.getTokenPrefix() + Jwts.builder()
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, jwtProperties.getApiSecretKey())
.compact();
return AccessToken.builder().loginAccount(subject).token(token).expirationTime(expirationDate).build();
}
public boolean validateToken(String token, UserDetails userDetails) {
Claims claims = getClaimsFromToken(token);
return claims.getSubject().equals(userDetails.getUsername()) && !isTokenExpired(claims);
}
public AccessToken refreshToken(String oldToken) {
String token = oldToken.substring(jwtProperties.getTokenPrefix().length());
// token反解析
Claims claims = getClaimsFromToken(token);
//如果token在15分钟之内刚刷新过,返回原token
if (tokenRefreshJustBefore(claims)) {
return AccessToken.builder().loginAccount(claims.getSubject()).token(oldToken).expirationTime(claims.getExpiration()).build();
} else {
return createToken(claims.getSubject());
}
}
private boolean tokenRefreshJustBefore(Claims claims) {
Date refreshDate = new Date();
//刷新时间在创建时间的指定时间内
if (refreshDate.after(claims.getExpiration()) && refreshDate.before(DateUtil.offsetSecond(claims.getExpiration(), 900))) {
return true;
}
return false;
}
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(jwtProperties.getApiSecretKey())
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
log.error("JWT反解析失败, token已过期或不正确, token : {}", token);
}
return claims;
}
public String getSubjectFromToken(String token) {
Claims claims = getClaimsFromToken(token);
if (claims != null) {
return claims.getSubject();
} else {
return null;
}
}
private boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
}
UserContext
public class UserContext {
public static UserDetail getCurrentUser() {
return (UserDetail)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
public static String getUsername(){
return getCurrentUser().getUsername();
}
public static String getUserId(){
return getCurrentUser().getUserId();
}
}
UserDetail
@Data
public class UserDetail implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
private UserInfo userInfo;
private List roleInfoList;
private Collection extends GrantedAuthority> grantedAuthorities;
private List roles;
public String getUserId() {
return this.userInfo.getUserId();
}
public UserInfo getUserInfo() {
return userInfo;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
if (grantedAuthorities != null) {
return this.grantedAuthorities;
}
List grantedAuthorities = new ArrayList<>();
List authorities = new ArrayList<>();
roleInfoList.forEach(role -> {
authorities.add(role.getRoleId());
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleId()));
});
this.grantedAuthorities = grantedAuthorities;
this.roles = authorities;
return this.grantedAuthorities;
}
@Override
public String getPassword() {
return this.userInfo.getPassword();
}
@Override
public String getUsername() {
return this.userInfo.getAccount();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return userInfo.getDeleted() == 0;
}
}
具体流程