SpringSecurity,JWT简介
1.什么是SpringSecurity
SpringSecurity是一个功能强大且高度可定制的身份验证和访问控制框架,提供了完善的认证机制和方法级的授权功能,是一款非常优秀的权限管理框架,他的核心是一组过滤链,不同的功能经由不同的过滤器
2.什么是JWT
Json Web Token(JWT) 是为了在网络应用环境间传递声明而执行的一种基于json的开放标准(RFC 7519),token设计为紧凑且安全,特别适用于分布式站点的单点登录(SSO)场景
jwt实际是一个字符串,由头部,载荷,签名组成,用[.]分隔,在服务器直接根据token取出保存的用户信息,即可对token的可用性进行校验,使得单点登录更简单
SpringBoot 集成 SpringSecurity+JWT步骤
1.项目结构
2.数据库和实体类以及准备【注意:】为了方便只用了两张表,实体类省略
users表:
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`user_id` int NOT NULL AUTO_INCREMENT,
`user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`user_pwd` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
role表:
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
3.创建UserDetailsServiceImpl,编写登录操作
package cn.zb.security.service.impl;
import cn.zb.security.entity.Role;
import cn.zb.security.entity.Users;
import cn.zb.security.service.RoleService;
import cn.zb.security.service.UsersService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UsersService usersService;
@Autowired
private RoleService roleService;
/**
* 用户登录
* @param s 获取到的UserName,
* @return user 返回的是org.springframework.security.core.userdetails.User;
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if (s==null || "".equals("s")){
throw new RuntimeException("用户不能为空");
}
//根据用户名查询对象
Users user = usersService.findUserByUserName(s);
if (user==null){
throw new RuntimeException("用户不存在");
}
List<SimpleGrantedAuthority> authorities=new ArrayList<>();
//根据userId获取Role对象
List<Role> roles = roleService.findRoleByUserId(user.getUserId());
for (Role role:roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getName()));
}
//这里没有对用户密码进行判断,将数据库中查到的密码封装到了对象中,在WebSecurityConfig中configure()方法中,User进行了验证,
//前端传递的密码是明文,数据中存的是暗文,需要对前端传递的密码加密,进行验证,加密方法是new BCryptPasswordEncoder().encode("pwd")
return new User(user.getUserName(),user.getUserPwd(),authorities);
}
}
4.创建JwtUtils工具类,做token的生成和校验
package cn.zb.security.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
public class JwtUtils {
public static final String TOKEN_HEADER="Authorization"; //token请求头
public static final String TOKEN_PREFIX="Bearer";//token前缀
public static final long EXPIRATION=60*60*1000; //token有效期
public static final String SUBJECT="piconjo"; //签名主题
public static final String HEADER_STRING="Passport"; //配置token在http heads中的键值
public static final String APPSECRET_KEY="piconjo_secret"; //应用密钥
public static final String ROLE_CLAIMS="role"; //角色权限声明
//生成token
public static String createToken(String username,String role){
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS,role);
String token=Jwts.builder()
.setSubject(username)
.setClaims(map)
.claim("username",username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis()+EXPIRATION))
.signWith(SignatureAlgorithm.HS512,APPSECRET_KEY)
.compact();
return token;
}
//校验token
public static Claims checkJWT(String token){
try{
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
//从Token中获取username
public static String getUsername(String token){
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("username").toString();
}
//从Token中获取用户角色
public static String getUserRole(String token){
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("role").toString();
}
//校验Token是否过期
public static boolean isExpiration(String token){
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.getExpiration().before(new Date());
}
}
5.创建拦截器JWTAuthenticationFilter
springsercurity对UserDetailsServiceImpl返回的user进行验证,验证成功则生成token,失败则返回失败信息
package cn.zb.security.util;
import com.alibaba.fastjson.JSON;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter (AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
//默认的登录路径是/login,post请求
super.setFilterProcessesUrl("/api/login");
}
//验证操作, 接受并解析用户凭证
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//从输入流中获取到登录的信息
//创建一个token并条用authenticationManager.authenticate 让Spring Security进行验证
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
request.getParameter("username"),request.getParameter("password")));
}
/**
* 验证【成功】后调用的方法
* 若验证成功 生成token并返回
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
User user= (User) authResult.getPrincipal();
System.out.println(user.toString());
// 从User中获取权限信息
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
// 创建Token
String token = JwtUtils.createToken(user.getUsername(), authorities.toString());
// 设置编码 防止乱码问题
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// 在请求头里返回创建成功的token
// 设置请求头为带有"Bearer "前缀的token字符串
response.setHeader("token", JwtUtils.TOKEN_PREFIX + token);
// 处理编码方式 防止中文乱码
response.setContentType("text/json;charset=utf-8");
// 将反馈塞到HttpServletResponse中返回给前台
response.getWriter().write(JSON.toJSONString("登录成功"));
}
/**
* 验证【失败】调用的方法
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
String returnData="";
// 账号过期
if (failed instanceof AccountExpiredException) {
returnData="账号过期";
}
// 密码错误
else if (failed instanceof BadCredentialsException) {
returnData="密码错误";
}
// 密码过期
else if (failed instanceof CredentialsExpiredException) {
returnData="密码过期";
}
// 账号不可用
else if (failed instanceof DisabledException) {
returnData="账号不可用";
}
//账号锁定
else if (failed instanceof LockedException) {
returnData="账号锁定";
}
// 用户不存在
else if (failed instanceof InternalAuthenticationServiceException) {
returnData="用户不存在";
}
// 其他错误
else{
returnData="未知异常";
}
// 处理编码方式 防止中文乱码
response.setContentType("text/json;charset=utf-8");
// 将反馈塞到HttpServletResponse中返回给前台
response.getWriter().write(JSON.toJSONString(returnData));
}
}
6.创建拦截器JWTAuthorizationFilter
对前端传递的请求拦截,取出里面的token做token校验,获取UsernamePasswordAuthenticationToken对象返回springsecurity,进行相关的角色判断
package cn.zb.security.util;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String tokenHeader=request.getHeader(JwtUtils.TOKEN_HEADER);
//因为设置拦截全部请求,所有这里没有token放行之后是会返回没有登录
if (tokenHeader==null || !tokenHeader.startsWith(JwtUtils.TOKEN_PREFIX)){
chain.doFilter(request,response);
return;
}
//若请求头中由token,则调用下面的方法进行解析,并设置认证信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
super.doFilterInternal(request,response,chain);
}
public UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader){
//去掉前缀,获取token字符串
String token = tokenHeader.replace(JwtUtils.TOKEN_PREFIX, "");
//从token中解密获取用户用户名
String username=JwtUtils.getUsername(token);
//从token中解密获取用户角色
String role=JwtUtils.getUserRole(token);
String[] roles = StringUtils.strip(role, "[]").split(",");
List<SimpleGrantedAuthority> authorities=new ArrayList<>();
for (String s : roles) {
authorities.add(new SimpleGrantedAuthority(s));
}
if (username!=null){
//这里像SpringSecurity声明用户角色,做相关操作放行
return new UsernamePasswordAuthenticationToken(username,null,authorities);
}
return null;
}
}
7.创建WebSecurityConfig配置类,对springsecurity进行相关配置
package cn.zb.security.config;
import cn.zb.security.util.JWTAuthenticationEntryPoint;
import cn.zb.security.util.JWTAuthenticationFilter;
import cn.zb.security.util.JWTAuthorizationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
/**
* 安全配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
//跨域伪造请求限制无效
.csrf().disable()
.authorizeRequests()
//访问/data路径下的请求需要admin
.antMatchers("/data/**").hasRole("admin")
//拦截所有请求
.antMatchers("/").authenticated()
//对登录做放行,生成token
.antMatchers("/api/login").permitAll()
.and()
//添加jwt登录拦截器
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
//添加jwt鉴权拦截器
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement()
//关闭session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//异常处理
.exceptionHandling()
.authenticationEntryPoint(new JWTAuthenticationEntryPoint());
}
//使用BCryptPasswordEncoder密码加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//跨域配置
@Bean
CorsConfigurationSource corsConfigurationSource(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//注册跨域配置
source.registerCorsConfiguration("/**",new CorsConfiguration().applyPermitDefaultValues());
return source;
}
//静态资源放行
@Override
public void configure(WebSecurity web) {
// 设置拦截忽略文件夹,可以对静态资源放行
//web.ignoring().antMatchers("/images/**");
}
}
8.创建Controller类,进行代码测试
我们在WebSecurityConfig 配置过,想要访问/data下的方法需要用户拥有admin角色
package cn.zb.security.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/data")
public class UserController {
@GetMapping("/test")
private String data() {
return "访问成功";
}
}
9.操作截图
用admin和as两个用户,其中as是没有admin权限的