1 引包
implementation("org.springframework.boot:spring-boot-starter-security")//security
implementation('io.jsonwebtoken:jjwt:0.9.1')//jwt
2 配置 SecurityConfig
package com.microcardio.mp.common.config.auth;
import com.microcardio.mp.common.config.auth.exception.AdminAuthenticationEntryPoint;
import com.microcardio.mp.common.config.auth.exception.UrlAccessDeniedHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* <p> Security 核心配置类 </p>
*
* @author aaaak
* @since 2021-02-26
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 访问权限认证异常处理
*/
private final AdminAuthenticationEntryPoint adminAuthenticationEntryPoint;
/**
* 角色权限认证异常处理
*/
private final UrlAccessDeniedHandler urlAccessDeniedHandler;
/**
* 权限配置
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//允许该路径 类型请求通过
http.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll();
//正则匹配 public 开头的接口都要走 认证
http.authorizeRequests()
.regexMatchers("^/1admin").access("hasRole('ADMIN') or hasRole('USER')")
.antMatchers("/auth/user/**").permitAll()//后台登录 跳过 auth
.antMatchers("/wx/msg/**").permitAll()//微信消息服务 跳过 auth
.antMatchers("/api/**").permitAll() // 微信端 接口 跳过 auth
.antMatchers("/public/**").permitAll() // 公共接口 跳过 auth
.anyRequest().authenticated();
// 未登录的请求,请求失败处理
http.exceptionHandling().authenticationEntryPoint(adminAuthenticationEntryPoint);
// 未授权的请求,请求失败处理
http.exceptionHandling().accessDeniedHandler(urlAccessDeniedHandler);
// 认证策略, jwt, basic auth, digest aut 中选取 jwt 认证
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//禁用CSRF 允许跨域
http.csrf().disable().cors();
//除非明确列出,否则不要使用任何默认标题。
http.headers().defaultsDisabled().cacheControl();
//自定义权限拦截 jwt
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
// TODO: 2021/3/2 后期有需求再添加动态角色权限配置
}
/**
* 暂时性 将 登录用户信息存在内存中 测试
*
*/
// @Override
// protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// // 在内存中配置用户,配置多个用户调用`and()`方法
// auth.inMemoryAuthentication()
// .passwordEncoder(passwordEncoder()) // 指定加密方式
// .withUser("admin").password(passwordEncoder().encode("wxby123456")).roles("ADMIN")
// .and()
// .withUser("test").password(passwordEncoder().encode("wxby123456")).roles("USER");
//
// }
@Bean
public PasswordEncoder passwordEncoder() {
// BCryptPasswordEncoder:Spring Security 提供的加密工具,可快速实现加密加盐
return new BCryptPasswordEncoder();
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}
/**
* 忽略拦截url或静态资源文件夹 - web.ignoring(): 会直接过滤该url - 将不会经过Spring Security过滤器链 而且 只能过滤GET 请求
* http.permitAll(): 不会绕开springsecurity验证,相当于是允许该路径通过
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(HttpMethod.GET,
"/favicon.ico",
"/actuator/**",
"/v2/api-docs",
"/configuration/ui",
"/swagger-resources/**",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**",
"/wxAuth/**",
"/api/**");
}
}
3配置 认证授权 捕获 返回信息类
package com.microcardio.mp.common.config.auth.exception;
import com.microcardio.mp.common.entity.JsonResult;
import com.microcardio.mp.common.util.ResponseUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 认证失败处理类 返回未授权
* 用来解决匿名用户访问无权限资源时的异常
*/
@Configuration
@Slf4j
public class AdminAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
// 未登录 或 token过期
if (e instanceof InsufficientAuthenticationException) {
ResponseUtils.out(response, JsonResult.failed(-100, "令牌失效 请登陆 获取 新的访问令牌"));
} else if (e instanceof BadCredentialsException) {
ResponseUtils.out(response, JsonResult.failed(-100, "认证失败,密码错误"));
}
log.warn("adapter Exception: {}", e.getClass().getName());
}
}
package com.microcardio.mp.common.config.auth.exception;
import com.microcardio.mp.common.entity.JsonResult;
import com.microcardio.mp.common.util.ResponseUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.file.AccessDeniedException;
/**
* <p> 认证url权限 - 登录后访问接口无权限 - 自定义403无权限响应内容 </p>
*/
@Configuration
@Slf4j
public class UrlAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, org.springframework.security.access.AccessDeniedException e) throws IOException, ServletException {
log.warn("UrlAccess Exception: {}", e.getClass().getName());
ResponseUtils.out(response, JsonResult.failed(-101, e.getMessage()));
}
}
4编写权限验证 Filter
package com.microcardio.mp.common.config.auth;
import com.microcardio.mp.admin.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7, headerAuth.length());
}
return null;
}
}
5 编写用户信息 实体类 实现UserDetails
package com.microcardio.mp.admin.service;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.microcardio.mp.admin.entity.User;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
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.Objects;
import java.util.stream.Collectors;
@Getter
@Setter
public class UserDetailsImpl implements UserDetails {
private static final long serialVersionUID = 1L;
private Integer id;
private String username;
private String email;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserDetailsImpl(Integer id, String username, String email, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserDetailsImpl build(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
return new UserDetailsImpl(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public Integer getId() {
return id;
}
public String getEmail() {
return email;
}
@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;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
UserDetailsImpl user = (UserDetailsImpl) o;
return Objects.equals(id, user.id);
}
}
6 编写用户信息 Service类 实现UserDetailsService
此处是写死的没有查数据库
package com.microcardio.mp.admin.service.impl;
import com.microcardio.mp.admin.entity.Role;
import com.microcardio.mp.admin.entity.User;
import com.microcardio.mp.admin.service.UserDetailsImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashSet;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
PasswordEncoder passwordEncoder;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = new User();
if ("admin".equals(username)) {
HashSet<Role> roles = new HashSet<>();
roles.add(new Role().setName("ADMIN").setId(1));
user.setId(1).setUsername("admin").setPassword(passwordEncoder.encode("123456")).setEmail("admin@microcardio.com").setRoles(roles);
} else {
HashSet<Role> roles = new HashSet<>();
roles.add(new Role().setName("USER").setId(2));
user.setId(1).setUsername("user").setPassword(passwordEncoder.encode("123456")).setEmail("admin@microcardio.com").setRoles(roles);
}
return UserDetailsImpl.build(user);
}
}
7 JwtUtils
工具类 ,用于Signature
的生成
package com.microcardio.mp.common.config.auth;
import com.microcardio.mp.admin.service.UserDetailsImpl;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.security.core.userdetails.User;
import java.util.Date;
@Component
public class JwtUtils {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
@Value("test-aaaak-SecretKey")
private String jwtSecret;
@Value("86400000")
private int jwtExpirationMs;
public String generateJwtToken(Authentication authentication) {
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();
return Jwts.builder()
.setSubject((userPrincipal.getUsername()))
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
logger.error("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
}
8 测试认证登录
package com.microcardio.mp.admin.controller;
import com.microcardio.mp.admin.entity.JwtResponse;
import com.microcardio.mp.admin.entity.User;
import com.microcardio.mp.admin.service.UserDetailsImpl;
import com.microcardio.mp.admin.service.impl.UserDetailsServiceImpl;
import com.microcardio.mp.common.config.auth.JwtUtils;
import com.microcardio.mp.common.entity.JsonResult;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/auth/user")
@Api(tags = "admin 后台登录模块")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
PasswordEncoder encoder;
@Autowired
JwtUtils jwtUtils;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
PasswordEncoder passwordEncoder;
/**
* 登录 认证
*
* @param loginRequest
* @return
*/
@PostMapping("/signin")
public JsonResult<JwtResponse> authenticateUser(@Valid @RequestBody User loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtils.generateJwtToken(authentication);
UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal();
List<String> roles = user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
return JsonResult.success(new JwtResponse(jwt, user.getId(), user.getUsername(), user.getEmail(), roles));
}
}
9 验证 是否成功
Spring Security
登录
Spring Security
访问需要 验证身份接口
失败例子 未 头部带token
成功例子 头部带token
10 Swagger
集成 Spring Security
@Bean()
public Docket authDocket() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("xxxx-xx端接口文档")
.description("Restful API")
.version("1.0")
.build();
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.consumes(DEFAULT_PRODUCES_AND_CONSUMES)
.produces(DEFAULT_PRODUCES_AND_CONSUMES)
.groupName("管理后台接口")
.securityContexts(Lists.newArrayList(securityContext()))
.securitySchemes(Lists.newArrayList(securityScheme()))
.useDefaultResponseMessages(false)
.select()
.apis(RequestHandlerSelectors.any())
.paths(Predicates.or(PathSelectors.regex("/admin/*.*"),PathSelectors.regex("/auth/*.*")))
.build();
}
private SecurityContext securityContext() {
return SecurityContext.builder().securityReferences(defaultAuth()).build();
}
private List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return Arrays.asList(new SecurityReference("JWT", authorizationScopes));
}
private ApiKey securityScheme() {
return new ApiKey("JWT", "Authorization", "header");
}