SpringSecurity
SpringSecurity是一个强大的可高度定制的认证和授权框架,对于Spring应用来说它是一套Web安全标签。SpringSecurity注重于Java应用提供认证和授权功能,像所有的Spring项目一样,它对自定义需求具有强大的扩展性。
JWT
JWT是JSON WEB TOKEN的缩写,它是基于RFC 7519标准定义的一种可以安全传输的JSON对象,由于使用了数字签名,所以是可信任和安全的。
JWT的组成
JWT token的格式:header.payload.singatue
- head中用于存放签名的生成算法
{"alg":"HS512"}
- payload中用于存放用户名、token的生成时间和过期时间
{"sub":"admin","created":1489079981339,"exp":1489684155}
- signature为以header和payload生产的签名,一旦header和payload被篡改,验证将失败
{"sub":"admin","created":1489079981339,"exp":1489684155}
JWT实现认证和授权的原理
- 用户调用登录接口,登录成功后获取JWT的Token
- 之后用户每次调用接口都在http的header中添加一个叫Authorization的头,值为JWT的token
- 后台程序通过对Authorization头中信息的解码及数字签名校验来获取其中的用户信息,从而实现认证和授权。
JWT的工作流程
- 用户导航到登录页面,输入用户名和密码进行登录
- 服务器在验证登录授权,如果用户合法,则会根据用户的信息和服务器所定义好的规则生成JWT Token
- 服务器将该Token以JSON的格式进行返回
- 用户得到Token后,存储在localStorage或者以其它的数据格式存储
- 以后用户每次请求时,需要在请求的header中加入Authorization:Bearer token
- 服务器对Token进行校验,合法就解析其中的内容,然后根据拥有的权限和业务逻辑给出对应的响应结果
- 用户注销后,令牌将在客户端进行销毁,不需要与服务器进行交互。
Authentication和Authorization的区别:
Authentication:用户认证,应用程序通过帐号和密码确认用户
Authorization:授权,在用户确认身份之后提供权限,比如管理员可以修改数据,其它用户只允许阅读数据
基于SpringSecurity与JWT Token的使用
下载地址: https://gitee.com/zhou_yunong/mall-learning.git
项目使用表说明
- ums_admin:后台用户表
- ums_role:后台用户角色表
- ums_permission:后台用户权限表
- ums_admin_role_relation:后台用户和角色关系表,用户与角色是多对多关系
- ums_role_permission_relation:后台用户角色和权限关系表,角色与权限是多对多关系
- ums_admin_permission_relation:后台用户和权限关系表(除角色中定义的权限以外的加减权限),加权限是指用户比角色多出的权限,减权限是指用户比角色少的权限
所需要的表说明
- JwtTokenUtil(JWT Token生成工具类)
@Component
public class JwtTokenUtil {
//定义Log4J文件
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
//JWT密钥
@Value("${jwt.secret}")
private String secret;
//JWT的过期时间
@Value("${jwt.expiration}")
private Long expiration;
/**
* 根据负责生成JWT的Token
*/
private String generateToken(Map<String,Object> claims) {
return Jwts.builder()
.setClaims(claims) //自定义属性
.setExpiration(generateExpirationDate()) //过期时间
.signWith(SignatureAlgorithm.HS512,secret) //签名算法和密匙
.compact();
}
/**
* 从Token中获取到JWT的负载,解析JWT
* @param token
* @return
*/
private Claims getClaimsFromToken(String token) {
//相当于一个Map,包含了我们所需要的信息
Claims claims = null;
try {
claims = Jwts.parser()
//设置签名的密钥
.setSigningKey(secret)
//设置需要解析的Token
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败:{}",token);
}
/**
* sub(用户名)、created(创建时间、exp(过期时间)
* 解析结果:
* sub:admin
* created:1696877941
* exp:1482169527
*/
return claims;
}
/**
* 生成Token的过期时间
* @return
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 从token当中获取登录的用户名
* @param token
* @return
*/
public String getUserNameFromToken(String token) {
String userName;
try {
Claims claims = getClaimsFromToken(token);
userName = claims.getSubject();
} catch (Exception e) {
userName = null;
}
return userName;
}
/**
* 验证Token是否还有效
* @param token 客户端传递过来的Token
* @param userDetails 从数据库中查询用户的信息
* @return
*/
public Boolean validateToken(String token, UserDetails userDetails) {
String userName = getUserNameFromToken(token);
return userName.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token是否失效
* @param token
* @return
*/
private Boolean isTokenExpired(String token) {
Date expireDate = getExpiredDateFromToken(token);
//Token有效期小于当前时间返回TRUE(未过期),大于等于返回FALSE(已过期)
return expireDate.before(new Date());
}
/**
* 从Token当中获取过期的时间
* @param token
* @return
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 根据用户信息生成Token
* @param userDetails
* @return
*/
public String generateToken(UserDetails userDetails) {
Map<String,Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims);
}
/**
* 判断token是否可以被刷新
*/
public boolean canRefresh(String token) {
return !isTokenExpired(token);
}
/**
* token刷新
* @param token
* @return
*/
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims);
}
}
- SpringSecurity的配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UmsAdminService umsAdminService;
//当用户没有访问权限时的处理器,用于返回JSON格式的处理结果
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
//当未登录或Token失效时,返回JSON格式的结果
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
/**
* 用于配置需要拦截的URL路径、JWT过滤器及出异常后的处理器
* @param httpSecurity
* @throws Exception
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
//由于使用的是JWT,所以不需要csrf跨域
httpSecurity.csrf()
.disable()
//基于Token,因为不需要Session
.sessionManagement()
//SessionCreationPolicy用于管理Session的创建策略
//SpringSecurity永远不创建Session,不使用Session获取SecurityContext
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//用于授权,可开放什么路径,什么资源
.authorizeRequests()
.antMatchers(HttpMethod.GET, //允许对于网站静态资源的无授权访问
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources/**",
"/v2/api-docs/**"
)
.permitAll()
//登录注册允许匿名访问,不做权限
.antMatchers("/admin/login","/admin/register")
.permitAll()
//在跨域请求会先进行一次options请求(开放)
.antMatchers(HttpMethod.OPTIONS)
.permitAll()
//测试时全部运行访问
// .antMatchers("/**")
// .permitAll()
//除了上面的所有请求均需要授权认证
.anyRequest()
.authenticated();
//禁用缓存
httpSecurity.headers().cacheControl();
//添加JWT filter
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
}
/**
* 用于配置UserDetailService和PasswordEncode
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
//获取登录的用户信息
return username -> {
UmsAdmin admin = umsAdminService.getAdminByUsername(username);
if (admin != null) {
List<UmsPermission> permissionList = umsAdminService.getPermissionList(admin.getId());
return new AdminUserDetails(admin,permissionList);
}
throw new UsernameNotFoundException("用户名或者密码输入错误");
};
}
/**
* 在用户名和密码校验前添加的过滤器,如果有jwt的Token,会自动根据token信息进行登录
* @return
*/
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
- JwtAuthenticationTokenFilter(JWT登录授权器,每次登录都会请求这个类)
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
//获取Bearer -- Token
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());
String username = jwtTokenUtil.getUserNameFromToken(authToken);
log.info("checking username:{}",username);
//当token的username不为空是进行验证token是否为有效的token
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//从数据库得到带有密码完整的user信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
//判断Token是否有效
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
log.info("authenticated user:{}",username);
//设置用户登录状态
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
chain.doFilter(request,response);
}
}
- RestAuthenticationEntryPoint(未登录或Token访问失效时,自定义返回结果)
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
response.getWriter().flush();
}
}
- RestfulAccessDeniedHandler(当所访问的接口没有权限时,自定义返回结果)
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
response.getWriter().flush();
}
}
- AdminUserDetails(从Security获取用户数据)
public class AdminUserDetails implements UserDetails {
private UmsAdmin umsAdmin;
private List<UmsPermission> permissionList;
public AdminUserDetails(UmsAdmin umsAdmin, List<UmsPermission> permissionList) {
this.umsAdmin = umsAdmin;
this.permissionList = permissionList;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//返回当前用户的权限
return permissionList.stream()
.filter(permission -> permission.getValue()!=null)
.map(permission ->new SimpleGrantedAuthority(permission.getValue()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return umsAdmin.getPassword();
}
@Override
public String getUsername() {
return umsAdmin.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return umsAdmin.getStatus().equals(1);
}
}
- Application.yml(自定义JWT的Key)
#自定义jwt Key
jwt:
tokenHeader: Authorization #JWT存储的请求头
secret: mySecret #JWT加解密所使用的密钥
expiration: 604800 #JWT的超期限时间(60*60*24)
tokenHead: Bearer #JWT负载中拿到开头
- Swagger2配置(自动记住Authorization令牌)
/**
* Swagger2API文档的配置
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//为当前包下controller生成API文档
.apis(RequestHandlerSelectors.basePackage("com.mall.tiny.controller"))
//为有@Api注解的Controller生成API文档
// .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
//为有@ApiOperation注解的方法生成API文档
// .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build()
//添加登录认证
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("SwaggerUI演示")
.description("mall-tiny")
.contact("macro")
.version("1.0")
.build();
}
private List<ApiKey> securitySchemes() {
//设置请求头信息
List<ApiKey> result = new ArrayList<>();
ApiKey apiKey = new ApiKey("Authorization", "Authorization","header");
result.add(apiKey);
return result;
}
private List<SecurityContext> securityContexts() {
//设置需要登录认证的路径
List<SecurityContext> result = new ArrayList<>();
result.add(getContextByPath("/brand/.*"));
return result;
}
private SecurityContext getContextByPath(String pathRegex){
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(pathRegex))
.build();
}
private List<SecurityReference> defaultAuth() {
List<SecurityReference> result = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
result.add(new SecurityReference("Authorization", authorizationScopes));
return result;
}
}
- 给需要调用的接口添加访问权限
1、给查询接口添加pms:brand:read权限
2、给修改接口添加pms:brand:update权限
3、给删除接口添加pms:brand:delete权限
4、给添加接口添加pms:brand:create权限
@PreAuthorize("hasAuthority('pms:brand:read')")
认证与授权流程演示地址和所作笔记来源
链接: mall-learing.