目录
通过数据库动态加载用户信息
具体实现步骤
一.创建数据库
二.编写secutity配置类
三.编写controller
四.编写服务类实现UserDetailsService接口类
五.debug springboot启动类
认证过滤器
SpringSecurity内置认证流程
自定义认证流程
第一步:自定义一个类继承AbstractAuthenticationProcessingFilter类
第二步:把自定义的认证过滤器交给spring管理,并配置认证的路径
第三步:把自定义的认证过滤器配置在默认认证过滤器之前
第四步:测试
基于JWT实现无状态认证
第一步:编写生成jwt票据工具类
第二步:在认证成功后执行的方法里生成jwt令牌并返回给前端
第三步:结果
SpringSecurity基于Jwt实现认证小结
授权过滤器
授权流程
自定义授权过滤器流程
第一步:自定义一个类继承 OncePerRequestFilter类
第二步:把这个授权过滤器交给spring管理
第三步:测试
通过数据库动态加载用户信息
使用SpringSecurity时,访问某一个资源路径时,SpringSecurity会自动拦截,并跳转到登录页面(SpringSecurity提供),登录之后才可以访问指定的资源。
登录的这个过程就是用户的认证
认证的具体过程就是:Spring Security底层会自动调用UserDetailsService类型bean提供的用户信息与前端返回的用户信息进行合法比对,如果比对成功则资源放行,否则就认证失败;
所以我们需要创建一个UserDetailsService对象bean给spring容器管理
具体的步骤就是需要创建一个类实现UserDetailsService接口,重写loadUserByUsername()方法,
UserDetails loadUserByUsername(String userName)用户认证时自动调用这个方法,userName就是前端输入的明文密码
调用这个方法的目的就是加载一个用户类,让SpringSecurity底层拿去与前端返回的用户信息进行比对
在这个方法里面根据用户名去数据库查找这个用户的信息(用户名,用户密文密码,用户拥有的权限集合),然后封装到User类(实现了UserDetails接口)对象里,最后返回即可。
返回的这个用户信息就会被SpringSecurity底层自动调用去与前端返回的用户信息进行合法比对
总的来说就是:用户的认证是先调用loadUserByUsername方法,根据前端返回的用户名根据数据库查找这个用户的详细信息(用户名,用户密码密文,用户的权限集合),然后封装成一个UserDetails类,然后Spring Security底层再自动调用UserDetailsService类型bean提供的用户信息与前端返回的用户信息进行合法比对,如果比对成功则资源放行,否则就认证失败;
用户类中的是用户的密文密码,而前端返回的用户密码是密文,SpringSecurity要怎么比对呢
所以我们要配置BCryptPasswordEncoder加密相关bean,底层会自动调用这个bean把密码密文与密码明文进行匹配,所以我们存入数据库的密码密文需要使用BCrypt算法加密
具体实现步骤
一.创建数据库
create database security_demo default charset=utf8mb4;
use security_demo;
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(100) DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
`roles` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `tb_user` VALUES (1, 'hhh', '$2a$10$f43iK9zKD9unmgLao1jqI.VluZ.Rr/XijizVEA73HeOu9xswaUBXC', 'ROLE_ADMIN,P5');
INSERT INTO `tb_user` VALUES (2, 'aaa', '$2a$10$f43iK9zKD9unmgLao1jqI.VluZ.Rr/XijizVEA73HeOu9xswaUBXC', 'ROLE_SELLER,P7,ROLE_ADMIN');
密码都是123456经过BCrypt算法加密后的
二.编写secutity配置类
@Configuration
@EnableWebSecurity//开启web安全设置生效
//开启SpringSecurity相关注解支持
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//TODO:返回一个BCryptPasswordEncoder密码加密类类,让SpringSecurity自动调用把密码密文和明文进行匹配
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//开启默认form表单登录方式
.and()
.logout()//登出用默认的路径登出 /logout
.permitAll()//允许所有的用户访问登录或者登出的路径
.and()
.csrf().disable()//启用CSRF,防止CSRF攻击
.authorizeRequests();//授权方法,该方法后有若干子方法进行不同的授权规则处理
/* //允许所有账户都可访问(不登录即可访问),同时可指定多个路径
.antMatchers("/register").permitAll()//允许所有的用户访问
.antMatchers("/hello").hasAuthority("P1") //具有P5权限才可以访问
.antMatchers("/say").hasRole("SELECT") //具有ROLE_ADMIN 角色才可以访问,会自动加上ROLE_
.antMatchers("/aa","/bb").hasAnyAuthority("P1","ROLE_SELECT")//有任意一个权限都可以访问
.antMatchers("/aa","/bb").hasAnyRole("SELECT")//有任意一个权限都可以访问
.antMatchers("/aa","/bb").hasIpAddress("192.168.xxx.xxx")//必须是192.168.地址才能访问
.antMatchers("/aa","/bb").denyAll()//任何用户都不可以访问
.anyRequest().authenticated(); //除了上边配置的请求资源,其它资源都必须授权才能访问*/
}
}
三.编写controller
使用注解给每个资源接口授权,拥有指定权限才能进行访问
@RestController
public class UserController {
//拥有ROLE_ADMIN权限的用户才能访问此接口
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/hello")
public String hello(){
return "hello security";
}
//拥有ROLE_SELECT权限的用户才能访问此接口
@PreAuthorize("hasRole('SELECT')")
@GetMapping("/say")
public String say(){
return "say security";
}
@PermitAll//任何用户都可以访问此接口,不需要进行认证
@GetMapping("/register")
public String register(){
return "register security";
}
}
四.编写服务类实现UserDetailsService接口类
注意:
1.User是UserDetails接口的实现类,封装了用户权限相关的的数据及用户的权限数据, 不要导错包 ;
2.工程已经配置好了BCryptPasswordEncoder加密相关bean,底层会自动调用;
/**
* 创建一个UserDetailsService类bean,用户认证时自动调用loadUserByUsername方法加载用户信息
* 之后SpringSecurity会使用这个用户类与前端返回的用户信息进行合法比对,成功才能放行资源
*/
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private TbUserMapper tbUserMapper;
/**
* 通过用户名去数据库获取这个用户的具体信息(用户名,用户密文密码,用户的权限集合)创建一个用户类
* @param userName 前端返回的用户名字
* @return 返回一个用户类
* @throws UsernameNotFoundException 用户不存在的异常
*/
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//根据用户名去数据库查找用户的基本信息(用户名,密文密码,用户的权限集合)
TbUser tbUser = tbUserMapper.findByUserName(userName);
if(tbUser==null){
throw new UsernameNotFoundException("该用户不存在");
}
//封装UserDetails类,User是UserDetails接口的实现类
//使用工具类把用户的权限使用逗号进行分割
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(tbUser.getRoles());
UserDetails user = User.builder()
.username(tbUser.getUsername())
.password(tbUser.getPassword())
.authorities(authorities)
.build();
//返回这个用户类,之后SpringSecurity会使用这个用户类与前端的返回的用户信息进行比对
//自动调用BCryptPasswordEncoder bean把用户明文密码与用户密文密码进行比对
return user;
}
}
五.debug springboot启动类
访问hello接口(拥有ROLE_ADMIN权限才能访问),会自动跳到springsecurity提供的用户认证界面
会跳转到loadUserByUsername方法,并获取前端的用户名,然后使用这个用户名创建一个用户类
认证通过,即springSecurity会自动调用BCryptPasswordEncoder加密相关bean把密文密码和明文密码继续比对,比对通过就是认证通过,又因为hhh用户拥有ROLE_ADMIN权限,所以可以访问
认证过滤器
SpringSecurity内置认证流程
核心流程梳理如下:
- 认证过滤器(UsernamePasswordAuthentionFilter)接收form表单提交的账户、密码信息,并封装成UsernamePasswordAuthenticationToken认证凭对象;
- 认证过滤器调用认证管理器AuthenticationManager进行认证处理;
- 认证管理器通过调用用户详情服务获取用户详情UserDetails;
- 认证管理器通过密码匹配器PasswordEncoder进行匹配,如果密码一致,则将用户相关的权限信息一并封装到Authentication认证对象中;
- 认证过滤器将Authentication认证过滤器放到认证上下文,方便请求从上下文获取认证信息;
自定义认证流程
SpringSecurity内置的认证过滤器是基于post请求且为form表单的方式获取认证数据的,那如何接收前端Json异步提交的数据据实现认证操作呢?
显然,我们可仿照UsernamePasswordAuthentionFilter类自定义一个过滤器并实现认证过滤逻辑;
第一步:自定义一个类继承AbstractAuthenticationProcessingFilter类
在这个类中我们要重写 attemptAuthentication()方法,在这个方法中把前端收到的用户名,用户明文密码封装成 UsernamePasswordAuthenticationToken票据对象中,然后调用认证管理器认证这个用户的信息(就是使用UserDetailsService返回的用户类的密文密码通过密码匹配与用户明文密码进行比对)
我们还有重写一个构造器,在这个构造器中确定认证登录的地址
/**
* 自定义过滤器,继承AbstractAuthenticationProcessingFilter类
*/
public class MyUserNamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String USER_NAME="username";
private static final String PASSWORD="password";
/**
* 自定义构造器,传入认证登录的url地址
* @param loginUrl 登录的url地址
*/
public MyUserNamePasswordAuthenticationFilter(String loginUrl) {
super(loginUrl);
}
/**
* 尝试去认证的方法,把前端返回的用户名和密码封装到UsernamePasswordAuthenticationToken认证凭对象;
* data:{"username":"hhh","password":"123456"}
* @param request
* @param response
* @return
* @throws AuthenticationException
* @throws IOException
* @throws ServletException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//判断请求方法必须是post提交,且提交的数据的内容必须是application/json格式的数据
if (!request.getMethod().equalsIgnoreCase("POST") || !MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(request.getContentType()))
{
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//获取前端传入的json数据,并解析成map集合
//获取请求参数
//获取reqeust请求对象的发送过来的数据流
ServletInputStream in = request.getInputStream();
//将数据流中的数据反序列化成Map
HashMap<String,String> loginInfo = new ObjectMapper().readValue(in, HashMap.class);
String username = loginInfo.get(USER_NAME);
username = (username != null) ? username : "";
username = username.trim();
String password = loginInfo.get(PASSWORD);
password = (password != null) ? password : "";
//将用户名和密码信息封装到认证票据对象下
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
//调用认证管理器认证指定的票据对象
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* 认证成功后执行的方法
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//获取封装后的用户对象
//这一个principal对象里的密码为null
User principal=(User)authResult.getPrincipal();
String username = principal.getUsername();
Collection<GrantedAuthority> authorities = principal.getAuthorities();
//设置返回数据格式
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
//设置返回数据编码格式
response.setCharacterEncoding("UTF-8");
Map<String,String> info=new HashMap<>();
info.put("msg","认证成功");
info.put("code","1");
info.put("data","");
//把类对象变成json数据
String jsonData = new ObjectMapper().writeValueAsString(info);
//将json数据返回
response.getWriter().write(jsonData);
}
/**
* 认证失败执行的方法
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//设置响应数据为json
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
//设置响应数据格式
response.setCharacterEncoding("UTF-8");
//设置响应的数据
Map<String,String>info=new HashMap<>();
info.put("msg","认证失败");
info.put("code","0");
info.put("data","");
//将数据封装成json数据
String jsonData = new ObjectMapper().writeValueAsString(info);
response.getWriter().write(jsonData);
}
}
第二步:把自定义的认证过滤器交给spring管理,并配置认证的路径
在这个自定义的认证过滤器bean中,需要注入一个认证管理器的bean
/**
* 自定义认证过滤器
* 隐含:如果认证成功,则在安全上下文中维护认证相关信息
* 如果安全上下文中存在认证相关信息,则默认的UserNamePasswordAuthenticationFilter认证过滤器就不会执行
* 所以执行顺序,自定义的过滤器在前,默认的过滤器在后
* @return
* @throws Exception
*/
@Bean
public MyUserNamePasswordAuthenticationFilter myUserNamePasswordAuthenticationFilter() throws Exception {
//构造认证过滤器对象,并设置认证路径 /myLogin
MyUserNamePasswordAuthenticationFilter myUserNamePasswordAuthenticationFilter = new MyUserNamePasswordAuthenticationFilter("/myLogin");
//注入一个认证管理器bean
myUserNamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
return myUserNamePasswordAuthenticationFilter;
}
第三步:把自定义的认证过滤器配置在默认认证过滤器之前
因为只要自定义的认证过滤器认证成功,就会在安全上下文中维护认证成功的相关信息,这样就不会去执行默认的认证过滤器
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//开启默认form表单登录方式
.and()
.logout()//登出用默认的路径登出 /logout
.permitAll()//允许所有的用户访问登录或者登出的路径
.and()
.csrf().disable()//启用CSRF,防止CSRF攻击
.authorizeRequests();//授权方法,该方法后有若干子方法进行不同的授权规则处理
//对于自定义过滤器,需要创建一个实例,所以用方法返回一个自定义认证过滤器
http.addFilterBefore(myUserNamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
/* //允许所有账户都可访问(不登录即可访问),同时可指定多个路径
.antMatchers("/register").permitAll()//允许所有的用户访问
.antMatchers("/hello").hasAuthority("P1") //具有P5权限才可以访问
.antMatchers("/say").hasRole("SELECT") //具有ROLE_ADMIN 角色才可以访问,会自动加上ROLE_
.antMatchers("/aa","/bb").hasAnyAuthority("P1","ROLE_SELECT")//有任意一个权限都可以访问
.antMatchers("/aa","/bb").hasAnyRole("SELECT")//有任意一个权限都可以访问
.antMatchers("/aa","/bb").hasIpAddress("192.168.xxx.xxx")//必须是192.168.地址才能访问
.antMatchers("/aa","/bb").denyAll()//任何用户都不可以访问
.anyRequest().authenticated(); //除了上边配置的请求资源,其它资源都必须授权才能访问*/
}
第四步:测试
访问myLogin接口
基于JWT实现无状态认证
JWT是无状态的,所以在服务器端无需存储和维护认证信息,这样会大大减轻服务器的压力,所以我们可在自定义的认证过滤器认证成功后通过successfulAuthentication方法向前端颁发token认证字符串;
第一步:编写生成jwt票据工具类
public class JwtTokenUtil {
// Token请求头
public static final String TOKEN_HEADER = "authorization";
// Token前缀
public static final String TOKEN_PREFIX = "Bearer ";
// 签名主题
public static final String SUBJECT = "JRZS";
// 过期时间,单位毫秒
public static final long EXPIRITION = 1000 * 60 * 60* 24 * 7;
// 应用密钥
public static final String APPSECRET_KEY = "hhha";
// 角色权限声明
private static final String ROLE_CLAIMS = "role";
/**
* 生成Token
*/
public static String createToken(String username,String role) {
Map<String,Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
String token = Jwts
.builder()a
.setSubject(username)
.setClaims(map)
.claim("username",username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
.signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
return token;
}
/**
* 校验Token
*/
public static Claims checkJWT(String token) {
try {
final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 从Token中获取用户名
*/
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());
}
}
第二步:在认证成功后执行的方法里生成jwt令牌并返回给前端
/**
* 认证成功后执行的方法
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//获取封装后的用户对象
//这一个principal对象里的密码为null
User principal=(User)authResult.getPrincipal();
String username = principal.getUsername();
Collection<GrantedAuthority> authorities = principal.getAuthorities();
//使用工具类生成jwt令牌,authorities.toString()把权限集合变成["P1","ROLE_ADMIN]类型
//构建JwtToken 加入权限信息是为了将来访问时,jwt解析获取当前用户对应的权限,做授权的过滤
String token = JwtTokenUtil.createToken(username, authorities.toString());
//设置返回数据格式
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
//设置返回数据编码格式
response.setCharacterEncoding("UTF-8");
Map<String,String> info=new HashMap<>();
info.put("msg","认证成功");
info.put("code","1");
info.put("data",token);
//把类对象变成json数据
String jsonData = new ObjectMapper().writeValueAsString(info);
//将json数据返回
response.getWriter().write(jsonData);
}
第三步:结果
SpringSecurity基于Jwt实现认证小结
授权过滤器
授权流程
自定义授权过滤器流程
第一步:自定义一个类继承 OncePerRequestFilter类
重写doFilterInternal方法,在这个方法中,获取请求头的token票据,并进行校验判断
/**
* 定义授权过滤器,本质就是获取一切请求头的token信息,进行校验
*/
public class AuthenticationFilter extends OncePerRequestFilter {
/**
* 过滤中执行的方法
* @param request
* @param response
* @param filterChain 过滤器链
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//1.从请求头中获取token字符串
String tokenStr = request.getHeader(JwtTokenUtil.TOKEN_HEADER);
//2.合法性判断
//2.1 判断票据信息是否为空
if(StringUtils.isBlank(tokenStr)){
//如果票据为空,则放行,但是此时安全上下文中没有认证成功的票据,后续的过滤器如果得不到票据,后面的认证过滤器会进行拦截
filterChain.doFilter(request,response);
return;
}
//2.2检查票据是否合法,解析失败
Claims claims = JwtTokenUtil.checkJWT(tokenStr);
if(claims==null){
//票据不合法,直接让过滤器终止
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
Map<String,String> info=new HashMap<>();
info.put("msg","票据无效,请重新认证");
info.put("data","");
info.put("code","0");
//把数据序列化成json格式
String jsonData = new ObjectMapper().writeValueAsString(info);
response.getWriter().write(jsonData);
return;
}
//2.3
//获取用户名
String username = JwtTokenUtil.getUsername(tokenStr);
//获取用户权限集合 "["P1","ROLE_ADMIN"]"
String roles = JwtTokenUtil.getUserRole(tokenStr);
//解析用户权限字符串
String stripStr = StringUtils.strip(roles, "[]");
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(stripStr);
//3.组装数据到UsernamePasswordAuthenticationToken票据对象中
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,null,authorities);
//4.将封装的认证票据存入security安全上下文中,这样后续的过滤器就可以直接从安全上下文中获取用户的相关权限信息
//TODO:以线程为维度:当前访问结束,那么线程回收,安全上下文中的票据也会回收,下次访问时需要重新解析
SecurityContextHolder.getContext().setAuthentication(token);
//5.放行请求,后续的过滤器,比如:认证过滤器如果发现安全上下文中存在token票据对象,就不会进行重新认证
filterChain.doFilter(request,response);
}
}
第二步:把这个授权过滤器交给spring管理
让这个授权过滤器的优先级高于自定义的认证过滤器,因为如果之前已经认证过,请求头中就会存在token票据,授权过滤器就会解析token票据,并把用户名和用户权限封装到UsernamePasswordAuthenticationToken类对象中,然后把这个对象存入安全上下文中,这样一来后续的认证过滤器就不会重新认证,直接放行
/**
* 维护一个授权过滤器bean,检查jwt票据是否有效,并做相关处理
*/
@Bean
public AuthenticationFilter authenticationFilter(){
return new AuthenticationFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//开启默认form表单登录方式
.and()
.logout()//登出用默认的路径登出 /logout
.permitAll()//允许所有的用户访问登录或者登出的路径
.and()
.csrf().disable()//启用CSRF,防止CSRF攻击
.authorizeRequests();//授权方法,该方法后有若干子方法进行不同的授权规则处理
//对于自定义过滤器,需要创建一个实例,所以用方法返回一个自定义认证过滤器
http.addFilterBefore(myUserNamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
//配置授权过滤器,它是资源安全的屏障,优先级最高,所以需要在自定义的认证过滤器之前
http.addFilterBefore(authenticationFilter(), MyUserNamePasswordAuthenticationFilter.class);
}
第三步:测试
先通过/myLogin路径进行用户认证获取token票据
一开始会被授权过滤器拦截,但是请求头中票据为空,授权过滤器就会放行,进入到认证过滤器(因为访问路径是/myLogin),然后获取票据信息
使用该票据访问其他资源,访问成功
授权过滤器解析请求头的票据,把解析到的用户名,用户权限信息存入UsernamePasswordAuthenticationToken类对象,然后再将对象存入安全上下文中,这样后面的过滤器就知道这个用户有哪些权限