SpringSecurity+JWT权限认证

SpringSecurity默认的是采用Session来判断请求的用户是否登录的,但是不方便分布式的扩展
虽然SpringSecurity也支持采用SpringSession来管理分布式下的用户状态,不过现在分布式的还是无状态的Jwt比较主流

一、创建SpringBoot的项目

spring-boot-starter-web
spring-boot-starter-security

二、代码

server.port=8111

# 再配置文件中设置登录系统的账户、密码
spring.security.user.name=jordan
spring.security.user.password=jordan
/**
	通过配置类设置登录系统的账户、密码
*/
@Configuration
public class SecurityConfig extends WebSecurityConfig{
	@Override 
	protected void configure(AuthenticationManagerBuilder auth){
		//密码加密
		BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
		String password = passwordEncoder.encode("123");
		
		auth.inMemoryAuthentication().withUser("kobe").password(password).roles("admin");
	}

	@Bean
	PasswordEncoder password(){
		return new BCryptPasswordEncoder();
	}
}	
/**
	自定义配置类,设置用户名、密码
	该配置类设置使用哪个service实现类
*/
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter{

	@Autowired
	private UserDetailsService userDetailsService;
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception{
		auth.userDetailsService(userDetailsService).passwordEncoder(password());
	}

	@Bean
	PasswordEncoder password(){
		return new BCryptPasswordEncoder();
	}

	/**
		自定义登录页面
	*/
	@Override
	protected void configure(HttpSecurity http) throws Exception{
		
		//配置没有权限访问跳转自定义页面
		http.exceptionHandling().accessDeniedPage("/unauth.html");

		http.formLogin()//自定义自己编写的页面
			.loginPage("/login.html")//登录页面设置
			.loginProcessingUrl("/user/login")//登录访问路径
			.defaultSuccessUrl("/test/index").permitAll()//登录成功后跳转的路径
			.and().authorizeRequests()
			.antMatchers("/","test/hello","/user/login").permitAll()//设置哪些路径可以直接访问
			
		//	.antMatchers("test/index").hasAuthority("admins")//当前登录用户,只有具有admins角色的权限下才能访问该路径(单个角色)
			.antMatchers("/test/index").hasAnyAuthority("admins,manager")//	多个权限
			
			.antMatchers("/test/index").hasRole("sale") //单个角色
			.antMatchers("index").hasAnyRole("")//多个角色任意一个
			//.anyRequest().authenticated()
			.and().csrf().disable();//关闭csrf防护
	}
}

/**
	service实现类
*/
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService{

	@Override
	public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException{
		//设置权限
		List<GrantedAuthority> auths = AuthorityUtils.commaeparatedStringToAuthorityList("admins,ROLE_sale");//设置权限,和角色(角色必须又前缀ROLE_)
		return new User("james",new BCryptPasswordEncoder().encode("123"),auths);
	}
}
@RestController
@RequestMapping("/test")
public class TestController{
	@GetMapping("hello")
	public String add(){
		return "hello security";
	}
}

三、启动运行

通过浏览器访问http://localhost:8111
进入页面 user 默认密码从控制台中寻找(三种方式设置用户名和密码:配置文件方式、配置类、自定义编写实现类

===========================================================

查询数据库完成认证

一、添加依赖

mybatis-plus-boot-starter
mysql-connector-java
lombok

二、创建数据表(id,username,password)以及对应的实体类,配置文件中添加数据库连接信息
在这里插入图片描述
在这里插入图片描述

三、编写mapper,以及service

/**
	启动类上必须添加该接口所在的包扫描 @MapperScan("com.xxx.mapper")
*/
@Repository
public interface UsersMapper extends BaseMapper<User>{
	
}
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailService{
	
	@Autowired
	private UsersMapper usersMapper;

	@Override
	public UserDetails loadUserByUsername throws UsernameNotFoundException(String username){
		//调用usersMapper,查询数据库
		QueryWrapper<Users> wrapper = new QueryWrapper();
		wrapper.eq("username",username);//where username=? 
		Users users = usersMapper.selectOne(wrapper);
		
		if(users == null){ //数据库没有用户名,认证失败
			throw new UsernameNotFoundException("用户名不存在");
		}
		List<GrantAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("roles");
		return new User(users.getUsername(),newBCryptPasswordEncoder().encode(users.getPassword()),auths);
	}
	
}

=====================================================

注解

一、启动类上开启注解@EnableGlobalMethodSecurity(securedEnabled=true)

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled=true)
public class DemosecurityApplication{

}

二、控制类

@RestController
@RequestMapping("/test")
public class TestController{

	@GetMapping("update")
	@Secured("ROLE_sale","ROLE_manager")//设置角色
	public String update(){
		return "hello update";
	}
}

@RequestMapping("/preAuthorize")
@ResponseBody
@PreAuthorize("hasAnyAuthority('admin')")//进入方法前权限验证,将角色权限参数传到方法中
public String preAuthorize(){
	System.out.printn("preAuthorize");
	return "preAuthorize";
}

/**
	@PostAuthorize("hasAnyAuthority('admins')")//方法后权限验证,主要针对返回值
*/

/**
	@PostFilter 权限验证之后对数据进行过滤,留下用户名是admin1的数据
*/
@RequestMapping("getAll")
@PreAuthorize("hasRole('ROLE_管理员')")
@PostFilter("filterObject.username == 'admin'")
@ResponseBody
public List<UserInfo> getAllUser(){
	ArrayList<UserInfo> list = new ArrayList<>();
	list.add(new UserInfo(1l,"admin1","6666"));
	list.add(new UserInfo(2l,"admin2","888"));
	return list;
}

/**
	@PostFilter 权限验证之后对数据进行过滤,留下用户名是admin1的数据
*/

三、userDetailsService设置用户角色

List<GrantAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");

==============================================================

微服务权限案例

一、数据表

在这里插入图片描述

二、父工程,管理依赖版本。子工程以及子工程中的模块
在这里插入图片描述

父工程——pom

<properties>
	<!--确定各组件的依赖版本-->
	<java.version>1.8</java.version>
	<mybatis-plus.version>3.0.5</mybatis-plus.version>
</properties>

<dependencyManagement>
	<dependencies>
		<!--对上面的版本进行具体化的依赖-->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>${mybatis-plus.version}</verison>
		</dependency>
	</dependencies>
</dependencyManagement>

子工程common——pom

<dependencies>
	<!--对父工程的依赖进行去版本处理-->
	<dependency>
		<groupId>io.springfox</groupId>
		<artifactId>springfox-swagger2</artifactId>
		<scope>provided</scope>
	</dependency>
</dependencies>

三、启动redis,启动Nacos注册中心
在这里插入图片描述
在这里插入图片描述

四、代码

密码处理

@Component
public class DefaultPasswordEncoder implements PasswordEncoder{

	public DefaultPasswordEncoder(){
		
	}
	public DefaultPasswordEncoder(int strength){
		
	}
	
	/**
		对密码进行MD5加密
	*/
	@Override
	public String encode(CharSequence charSequence){
		return MD5.encrypt(charSequence.toString());
	}
	
	/**
		密码对比
	*/
	@Override
	public boolean matches(CharSequence charSequence,String s){
		return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
	}
}

token管理

@Component
public class TokenManager{

	//设置token有效时常
	private long takenEcpiration = 24*60*60*1000;
	//编码密钥(编码,解码)
	private String tokenSignKey = "123456";
	
	//使用jwt根据用户名生成token
	public String createToken(String username){
		String token = Jwts.builder().setSubject(username)
			.setExpiration(new Date(System.currentTimeMillis()+tokenEcpiration))//设置有效时长
			.signWith(SingatureAlgorithm.HS512,tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
		
		return token;
	}

	//根据token字符串得到用户信息
	public String getUserInfoFromToken(String token){
		String userinfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
		return userinfo;
	}

	//删除token
	public void removeToken(String token){}
}

退出登录

public class TokenLogoutHandler implements LogoutHandler{

	private TokenManager tokenManager;
	private RedisTemplate redisTemplate;

	public TokeLogoutHandler(TokenManager tokenManager,RedisTemplate redisTemplate){
		this.tokenManager = tokenManager;
		this.redisTemplate = redisTemplate;
	}
	
	@Override
	public void logout(HttpServletRequest request,HttpServletResponse response,Authentication authentication){
		//从header里面获取token
		String token = request.getHeader("token");
		if(token != null){
			tokenManager.removeToken(token);

			//从token获取用户名
			String username = tokenManager.getUserInfoFromToken(token);
			redisTemplate.delete(username);
		}
		ResponseUtil.out(response,R.ok());
	}
}

未授权统一处理类

public class UnauthEntryPoint implements AuthenticationEntryPoint{
	
	@Override
	public void commence(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,AuthenticationException e){
		ResponseUtil.out(httpServletResponse,R.error());
	}
}

认证和授权自定义过滤器

public class TokenLoginFilter extends UsernamePasswordAuthentiocationFilter{

	private TokenManager tokenManager;
	private RedisTemplate redisTemplate;
	private AuthenticationManager authenticationManager;

	//有参构造和无参构造(略)
	//构造函数中同时设定
	
	//获取表单提供的用户名和密码
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException{
		try{
			User user = new ObjectMapper().readValue(request.getInputStream(),User.class);
			return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(),new ArrayList<>())	);
		}catch(){}
	}

	//认证成功调用的方法
	@Override
	protected void seccessFulAuthentication(HttpServletRequest requeest,HttpervletResponse response,FilterChain chain,Authentication authResult) throws IOException,ServletException{
		//认证成功,得到用户信息
		SecurityUser user = (SecurityUser)authResult.getPrincipal();
		//根据用户生成token
		String token = tokenManager.createToken(user.getCurrentUerInfo().username());
		
	}

	//认证失败调用的方法
	@Override
	protected void unsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response,uthenticationException failed)throws IOException,ServletException{

	}
}

=============================================

工具类

public class AuthUtils{
	
	public static Authentication getPrincipal(){
		return SecurityContextHolder.getContext().getAuthentication();
	}
}
/**
	controller中进行应用
*/
@PostMapping("/app/user/coupons")
public Response<List<CouponResponse>> coupons(){
	LoginUser principal = (LoginUser) AuthUtils.getPrincipal().getPrincipal();
	return appDiscountCouponService.queryCoupons(principal.getId());
}

================================================================

SpringSecurity变成前后端分离,采用JWT做认证

什么是有状态:

有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如 Tomcat 中的 Session。例如登录:用户登录后,我们把用户的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session,然后下次请求,用户携带 cookie 值来(这一步有浏览器自动完成),我们就能识别到对应 session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:

  • 服务端保存大量数据,增加服务端压力
  • 服务端保存用户状态,不支持集群化部署

什么是无状态:

微服务集群中的每个服务,对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是:服务的无状态性,即:

  • 服务端不保存任何客户端请求者信息
  • 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份
  • 客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器
  • 服务端的集群和状态对客户端透明
  • 服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)
  • 减小服务端存储压力

无状态登录的流程:

  • 首先客户端发送账户名、密码到服务端进行认证
  • 认证通过后,服务端将用户信息加密并且编码成一个 token,返回给客户端
  • 以后客户端每次发送请求,都需要携带认证的 token
  • 服务端对客户端发送来的 token 进行解密,判断是否有效,并且获取用户登录信息

JWT,全称是 Json Web Token

轻量级的授权和身份认证规范,可实现无状态、分布式的 Web 应用授权:

常用的 Java 实现是 GitHub 上的开源项目 jjwt,地址如下:https://github.com/jwtk/jjw
在这里插入图片描述

JWT包含三部分数据:

①、Header:头部,通常头部有两部分信息(对头部进行 Base64Url 编码(可解码),得到第一部分数据)

  • 声明类型,这里是JWT
  • 加密算法,自定义

②、Payload:载荷,就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:(这部分也会采用 Base64Url 编码,得到第二部分数据)

  • iss (issuer):表示签发人
  • exp (expiration time):表示token过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

③、Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥secret(密钥保存在服务端,不能泄露给客户端),通过 Header 中配置的加密算法生成。用于验证整个数据完整和可靠性。

生成的数据格式如下图:(这里的数据通过 . 隔开成了三部分,分别对应前面提到的三部分,另外,这里数据是不换行的,图片换行只是为了展示方便而已)
在这里插入图片描述

JWT交互流程
在这里插入图片描述

  1. 应用程序或客户端向授权服务器请求授权
  2. 获取到授权后,授权服务器会向应用程序返回访问令牌
  3. 应用程序使用访问令牌来访问受保护资源(如 API)

因为 JWT 签发的 token 中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,这样就完全符合了 RESTful 的无状态规范

存在的问题:

  • 续签问题,这是被很多人诟病的问题之一,传统的 cookie+session 的方案天然的支持续签,但是 jwt 由于服务端不保存用户状态,因此很难完美解决续签问题,如果引入 redis,虽然可以解决问题,但是 jwt 也变得不伦不类了。
  • 注销问题,由于服务端不再保存用户信息,所以一般可以通过修改 secret 来实现注销,服务端 secret 修改后,已经颁发的未过期的 token 就会认证失败,进而实现注销,不过毕竟没有传统的注销方便。
  • 密码重置,密码重置后,原本的 token 依然可以访问系统,这时候也需要强制修改 secret。
  • 基于第 2 点和第 3 点,一般建议不同用户取不同 secret。

5个handler

  • 实现AuthenticationEntryPoint接口,当匿名请求需要登录的接口时拦截处理
  • AuthentiocationSuccessHandler接口,当登录成功后,该处理类的方法被调用
  • AuthenticationFailureHandler接口,当登录失败后,该处理类的方法被调用
  • AccessDeniedHandler接口,当登陆后,访问接口没有权限的时候该处理类的方法被调用
  • LogoutSuccessHandler接口,注销的时候调用

1个filter OncePerRequestFilter

前端发起请求的时候将token放在请求头中,在过滤器中对请求头进行解析

  • 如果有accessToken的请求头,取出token,解析成功,将解析出来的用户信息放到SpringSecurity的上下文中
  • 如果有accessToken的请求头,解析失败(无效token,或者过期失效)取不到用户信息,则放行
  • 没有accessToken的请求头,放行

AuthenticationEntryPoint

匿名未登录的时候访问,遇到需要登录认证的时候被调用

@Component
public class CustomerAuthenticationEntryPoint implements AuthentiocationEntryPoint{
	
	@Override
	 public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
     //设置response状态码,返回错误信息等
     ...
        ResponseUtil.out(401, ResultUtil.failure(ErrorCodeConstants.REQUIRED_LOGIN_ERROR));
    }
} 

AuthenticationSuccessHandler

输入用户名和密码认证成功后,调用的方法
获取用户信息,使用JWT生成token,然后返回token

@Slf4j
@Component
public class CustomerAuthentiocationSuccessHandler implements AuthenticationSuccessHandler{
	
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
		//获取当前用户,拿到用户名或者userId,创建token
		log.info("登录成功……");
		CustomerUserDetails principal = (CustomerUserDetails)authentication.getPrincipal();
		
		//颁发token
		Map<String,Object> emptyMap = new HashMap<>(4);
		emptyMap.put(UserConstants.USER_ID,principal.getId());

		String token = JwtTokenUtil.generateToken(principal.getUsername(),emptyMap);
		ResponseUtil.out(ResultUtil.success(token));
	}
}

AuthenticationFailureHandler

登录失败调用该方法

@Slf4j
@Component
public class CustomerAuthenticationFailHandler implements AuthenticationFailureHandler{
	
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
	
		//设置response状态码,返回错误信息等
     ....
        ResponseUtil.out(401, ResultUtil.failure(ErrorCodeConstants.LOGIN_UNMATCH_ERROR));
	}
}

LogoutSuccessHandler

退出登录的时候调用
JWT无法主动控制失效,可以采用JWT+session方式,比如删除存在在Redis的token

@Component
public class CustomerLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        ResponseUtil.out(ResultUtil.success("Logout Success!"));
    }
}

AccessDeniedHandler

登录之后,访问缺失权限的资源会调用

@Component
@Slf4j
public class CustomerRestAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
        ResponseUtil.out(403, ResultUtil.failure(ErrorCodeConstants.PERMISSION_DENY));
    }

}

OncePerRequestFilter

过滤器,在请求过来的时候,解析请求头中的token,再解析token得到用户信息,再存到SecurityContextHolder中

@Component
@Slf4j
public class CustomerJwtAuthenticationTokenFilter extends OncePerRequestFilter{
	
	@Autowired
	CustomerUserDetailService customerUserDetailService;
	
	@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
    	
    	String authHeader = request.getHeader(SecurityConstants.HEADER);

		if(authHeader != null && authHeader.startsWith(SecurityConstants.TOKEN_SPLIT)){
			 UserDetails userDetails = customerUserDetailService.loadUserByUsername(username);
                if (userDetails != null) {
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
		}
    }
    
}

配置WebSecurityConfigurerAdapter

将handler和filter注册到SpringSecurity中,同时配置一些放行的url

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)// 控制@Secured权限注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  /**
   * 这里需要交给spring注入,而不是直接new
   */
  @Autowired
  private PasswordEncoder passwordEncoder;
  @Autowired
  private CustomerUserDetailService customerUserDetailService;
  @Autowired
  private CustomerAuthenticationFailHandler customerAuthenticationFailHandler;
  @Autowired
  private CustomerAuthenticationSuccessHandler customerAuthenticationSuccessHandler;
  @Autowired
  private CustomerJwtAuthenticationTokenFilter customerJwtAuthenticationTokenFilter;
  @Autowired
  private CustomerRestAccessDeniedHandler customerRestAccessDeniedHandler;
  @Autowired
  private CustomerLogoutSuccessHandler customerLogoutSuccessHandler;
  @Autowired
  private CustomerAuthenticationEntryPoint customerAuthenticationEntryPoint;


 
  /**
   * 该方法定义认证用户信息获取的来源、密码校验的规则
   *
   * @param auth
   * @throws Exception
   */
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //auth.authenticationProvider(myauthenticationProvider)  自定义密码校验的规则

    //如果需要改变认证的用户信息来源,我们可以实现UserDetailsService
    auth.userDetailsService(customerUserDetailService).passwordEncoder(passwordEncoder);
  }


  @Override
  protected void configure(HttpSecurity http) throws Exception {
    /**
     * antMatchers: ant的通配符规则
     * ? 匹配任何单字符
     * * 匹配0或者任意数量的字符,不包含"/"
     * ** 匹配0或者更多的目录,包含"/"
     */
    http
            .headers()
            .frameOptions().disable();

    http
            //登录后,访问没有权限处理类
            .exceptionHandling().accessDeniedHandler(customerRestAccessDeniedHandler)
            //匿名访问,没有权限的处理类
            .authenticationEntryPoint(customerAuthenticationEntryPoint)
    ;

    //使用jwt的Authentication,来解析过来的请求是否有token
    http
            .addFilterBefore(customerJwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);


    http
            .authorizeRequests()
            //这里表示"/any"和"/ignore"不需要权限校验
            .antMatchers("/ignore/**", "/login", "/**/register/**").permitAll()
            .anyRequest().authenticated()
            // 这里表示任何请求都需要校验认证(上面配置的放行)


            .and()
            //配置登录,检测到用户未登录时跳转的url地址,登录放行
            .formLogin()
            //需要跟前端表单的action地址一致
            .loginProcessingUrl("/login")
            .successHandler(customerAuthenticationSuccessHandler)
            .failureHandler(customerAuthenticationFailHandler)
            .permitAll()

            //配置取消session管理,又Jwt来获取用户状态,否则即使token无效,也会有session信息,依旧判断用户为登录状态
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

            //配置登出,登出放行
            .and()
            .logout()
            .logoutSuccessHandler(customerLogoutSuccessHandler)
            .permitAll()
            
            .and()
            .csrf().disable()
    ;
  }


}

实战

一、创建一个项目(添加 Spring Security 依赖,添加 jjwt 依赖)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

二、创建用户对象

public class User implements UserDetails {

    private String username;
    private String password;
    private List<GrantedAuthority> authorities;
    
    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;
    }
    //省略getter/setter
}

三、Controller接口

设计是 /hello 接口可以被具有 user 角色的用户访问,
而 /admin 接口则可以被具有 admin 角色的用户访问

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello jwt !";
    }
    @GetMapping("/admin")
    public String admin() {
        return "hello admin !";
    }
}

四、JWT过滤器

  • 一个是用户登录的过滤器,在用户的登录的过滤器中校验用户是否登录成功,如果登录成功,则生成一个token返回给客户端,登录失败则给前端一个登录失败的提示。
  • 第二个过滤器则是当其他请求发送来,校验token的过滤器,如果校验成功,就让请求继续执行。
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter{
	
	protected JwtLoginFilter(String defaultFilterProcessesUrl,AuthenticationManager authenticationManager){
		super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
        setAuthenticationManager(authenticationManager);
	}

	/**
		从登录参数中提取出用户名密码,然后调用 AuthenticationManager.authenticate() 方法去进行自动校验
	*/
	@Override
	public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) 
	 	throws AuthenticationException, IOException, ServletException {
		
		User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
	    return getAuthenticationManager()
	       .authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
	}

	/**
		如果校验成功,就会来到 successfulAuthentication 回调中,在 successfulAuthentication 方法中,将用户角色遍历然后用一个 , 连接起来,
		然后再利用 Jwts 去生成 token,按照代码的顺序,生成过程一共配置了四个参数,分别是用户角色、主题、过期时间以及加密算法和密钥,
		然后将生成的 token 写出到客户端
	*/
	@Override
    protected void successfulAuthentication(HttpServletRequest req, 
    										HttpServletResponse resp,
    										FilterChain chain, 
    										Authentication authResult)
     	throws IOException, ServletException {
		
		Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();

		StringBuffer as = new StringBuffer();
		for(GrantedAuthority authority : authorities){
			as.append(authority.getAuthority())
                    .append(",");
		}

		//Jwts 去生成 token
		String jwt = Jwts.builder()
			.claim("authorities",as)//配置用户角色
			.setSubject(authResult.getName())
			.setExpiration(new Date(System.currentTimeMillis()+ 10 * 60 * 1000))
			.signWith(SignatureAlgorithm.HS512,"sang@123")
            .compact();

		//将生成的 token 写出到客户端
		resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(jwt));
        out.flush();
        out.close();
	}
	
	/**
		校验失败就会来到 unsuccessfulAuthentication 方法中,在这个方法中返回一个错误提示给客户端即可
	*/
	protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) 
					throws IOException, ServletException {
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write("登录失败!");
        out.flush();
        out.close();
    }
}
public class JwtFilter extends GenericFilterBean{
	
	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
		 throws IOException, ServletException {

		//首先从请求头中提取出 authorization 字段,这个字段对应的 value 就是用户的 token
		HttpServletRequest req = (HttpServletRequest) servletRequest;
        String jwtToken = req.getHeader("authorization");
        System.out.println(jwtToken);
        
        //将提取出来的 token 字符串转换为一个 Claims 对象
        Claims claims = Jwts.parser().setSigningKey("sang@123").parseClaimsJws(jwtToken.replace("Bearer",""))
                .getBody();
        String username = claims.getSubject();//获取当前登录用户名
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));//获取用户角色

		//创建一个 UsernamePasswordAuthenticationToken 放到当前的 Context 中,然后执行过滤链使请求继续执行下去
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
        SecurityContextHolder.getContext().setAuthentication(token);
        filterChain.doFilter(req,servletResponse);
	}
}

五、SpringSecurity配置

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
	
	/**
		未对密码进行加密,因此配置了 NoOpPasswordEncoder 的实例
	*/
	@Bean
	PasswordEncoder passwordEncoder(){
		return NoOpPasswordEncoder.getInstance();
	}

	/**
		未连接数据库,在内存中配置了两个用户,两个用户具备不同的角色。
	*/
	@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("admin")
                .password("123").roles("admin")
                .and()
                .withUser("sang")
                .password("456")
                .roles("user");
    }

	/**
		配置路径规则时, /hello 接口必须要具备 user 角色才能访问, /admin 接口必须要具备 admin 角色才能访问,
		POST 请求并且是 /login 接口则可以直接通过,其他接口必须认证后才能访问。
	*/
	 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/hello").hasRole("user")
                .antMatchers("/admin").hasRole("admin")
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new JwtLoginFilter("/login",authenticationManager()),UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class)
                .csrf().disable();//最后配置上两个自定义的过滤器并且关闭掉 csrf 保护
    }
}

六、测试
在这里插入图片描述

登录成功后返回的字符串就是经过 base64url 转码的 token,一共有三部分,通过一个 . 隔开,我们可以对第一个 . 之前的字符串进行解码,即 Header,如下:
在这里插入图片描述
再对两个 . 之间的字符解码,即 payload:
在这里插入图片描述
设置信息,由于 base64 并不是加密方案,只是一种编码方案,因此,不建议将敏感的用户信息放到 token 中
接下来再去访问 /hello 接口,注意认证方式选择 Bearer Token,Token 值为刚刚获取到的值,如下:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/174555.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【giszz笔记】产品设计标准流程【8】

&#xff08;续上回&#xff09; 真的没想到写了8个章节&#xff0c;想参考之前文章的&#xff0c;我把链接给到这里。 【giszz笔记】产品设计标准流程【7】-CSDN博客 【giszz笔记】产品设计标准流程【6】-CSDN博客 【giszz笔记】产品设计标准流程【5】-CSDN博客 【giszz笔…

Transformer——encoder

本文参考了b站的Eve的科学频道中的深入浅出解释Transformer原理和DASOU讲AI中的Transformer从零详解。 入浅出解释Transformer原理 Transformer从零详解 前言&#xff1a; 在自然语言识别中&#xff0c;之前讲过lstm&#xff0c;但是lstm有明显的缺陷&#xff0c;就是当文本过…

[SCTF 2021]rceme

文章目录 前置知识可变参数绕过create_function注入无字母数字RCE动态链接库so绕过disable_functions利用php原生类进行文件读取 解题过程 前置知识 可变参数绕过 PHP 在用户自定义函数中支持可变数量的参数列表。在 PHP 5.6 及以上的版本中&#xff0c;由 … 语法实现&#x…

redis的过期策略以及定时器的实现

Redis是客户端服务器结构的程序&#xff0c;客户端与服务器通过网络通信&#xff0c;所以对于keys *这种的操作在大型企业中不太建议&#xff0c;生产环境下的key会非常多&#xff0c;Redis是但现成的服务器&#xff0c;执行keys*的时间非常长&#xff0c;就会导致redis服务器阻…

同为科技(TOWE)桌面PDU插排:一款可以DIY定制的“超级插座”

当今社会&#xff0c;各种电子产品和家用电器已成为人们日常生活中不可或缺的一部分&#xff0c;在带给人们便利的同时&#xff0c;也使得电力使用变得更加频繁和重要。然而&#xff0c;当前市面上很多普通插座由于功能单一、材质粗劣、插口数量受限、充电速度过慢、插头间互相…

子虔与罗克韦尔自动化合作 进博会签约自动化净零智造联创中心

11月6日进博会现场&#xff0c;漕河泾罗克韦尔自动化净零智造联创中心合作协议签约暨合作伙伴&#xff08;第一批&#xff09;授牌仪式举办&#xff0c;子虔科技作为联创中心合作伙伴签约&#xff0c;携手共建智能制造&#xff0c;引领行业可持续发展。 图示&#xff1a;子虔科…

QTableView表头Header增加复选框Checkbox

原文出处&#xff1a;Qt 之 QHeaderView 添加复选框_qtableview添加复选框-CSDN博客 这哥们只贴了部分代码&#xff0c;我还是把它弄好分享给大家吧 DTableHeaderView.h #ifndef DTABLEHEADERVIEW_H #define DTABLEHEADERVIEW_H#include <QHeaderView>class DTableHea…

高精度人像背景分割SDK技术解决方案

图像处理技术已经成为企业和个人生活中不可或缺的一部分&#xff0c;特别是在人像处理方面&#xff0c;如何准确、高效地将人物与背景分离&#xff0c;一直是一个技术难题。然而&#xff0c;美摄科技凭借其在AI深度学习领域的深厚积累&#xff0c;推出了一款高精度的人像背景分…

app抓包-突破【单向证书验证代理检测模拟器检测】

0x00 app的普通抓包配置 1.模拟器开启本地以太网代理&#xff0c;设置端口&#xff0c;bp监听以太网系统端口即可 2.科来协议分析&#xff0c;可以获取模拟器进程的网络通讯信息&#xff0c;目标通讯的ip地址 3.封包监听工具&#xff0c;同科来一致&#xff0c;监听的进程即可…

gitlab

Gitlab 安装git yum安装 [rootgit ~]# yum -y install git编译安装 Git官网 #安装依赖关系 [rootgit ~]# yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel autoconf gcc perl-ExtUtils-MakeMaker # 编译安装 [rootgit ~]# tar -zxf git-2.0…

[汇编实操]DOSBox工具: unable to open input file: 文件名.asm问题解决

出错原因1 &#xff1a;将文件放在debug文件下&#xff0c;mount后发现并没有该文件 解决方案 &#xff1a;重启DOSBox&#xff0c;重新mount&#xff0c;直到dir后可以看到该asm文件 出错原因2&#xff1a;DOS系统不支持8位以上的文件名 解决方案 &#xff1a;将文件名改为8…

智能座舱架构与芯片- (14) 测试篇 上

一、 验证平台概要 1.1 测试软件方法论 “软件定义汽车” 的时代&#xff0c;软件在整车制造中的重要性日渐凸显。但不同于其他行业的软件开发&#xff0c;汽车行业有自己独特的软件开发要求。首先是需求严谨、需求层次复杂、需要通过专业的工具进行管理&#xff1b;其次开发…

基恩士软件的基本操作(四,快速编辑plc技巧)

目录 单元软原件注释快速添加 双击单元配置&#xff0c;进入单元编辑器 KV一键添加注释 双击软元件注释 进入软元件编辑界面 &#xff0c;对弹出的列表中软元件打勾点击登录 元件注释就自动添加了 注释收索&#xff0c;快速编辑软元件 自定义注释收索 空软元件快速查找 …

8.2 Windows驱动开发:内核解锁与强删文件

在某些时候我们的系统中会出现一些无法被正常删除的文件&#xff0c;如果想要强制删除则需要在驱动层面对其进行解锁后才可删掉&#xff0c;而所谓的解锁其实就是释放掉文件描述符&#xff08;句柄表&#xff09;占用&#xff0c;文件解锁的核心原理是通过调用ObSetHandleAttri…

PDF文件无密码,如何解密?

PDF文件有两种密码&#xff0c;一个打开密码、一个限制编辑密码&#xff0c;因为PDF文件设置了密码&#xff0c;那么打开、编辑PDF文件就会受到限制。想要解密&#xff0c;我们需要输入正确的密码&#xff0c;但是有时候我们可能会出现忘记密码的情况&#xff0c;或者网上下载P…

人工智能:科技之光,生活之美

在科技飞速发展的今天&#xff0c;人工智能已经深入到我们的生活中&#xff0c;它如同一束璀璨的科技之光&#xff0c;照亮我们生活的每一个角落&#xff0c;使我们的生活更加美好。下面我将从人工智能的领域、应用以及对人工智能的看法三个方面来谈谈它对我们生活的影响。 一、…

c语言-qsort函数的使用-参数带函数指针

一、qsort函数-数组排序 qsort函数是c库里的数组排序函数。会将原始数组进行升序排列。 头文件&#xff1a; #include <stdlib.h> 函数原型&#xff1a; void qsort(void *base, size_t nmemb, size_t size,int (*compar)(const void *, const void *)); 参数&#xff1a…

MySQL数据库:开源且强大的关系型数据库管理系统

大家好&#xff0c;我是咕噜-凯撒&#xff0c;数据在当今信息化时代的重要性不可忽视。作为企业和组织的重要资产&#xff0c;数据的管理和存储变得至关重要&#xff0c;MySQL作为一种关系型数据库管理系统&#xff0c;具有非常多的优势&#xff0c;下面简单的探讨一下MySQL数据…

[补题记录] Coolbits(2019陕西省赛)

URL&#xff1a;https://pintia.cn/problem-sets/91827364500/exam/problems/91827370530 目录 Problem/题意 Thought/思路 Code/代码 Problem/题意 给出 N 个区间&#xff0c;可以从每个区间中选择一个数&#xff0c;问选出的 N 个数的按位与的值最大是多少&#xff1f; …

文心一言 VS 讯飞星火 VS chatgpt (140)-- 算法导论11.4 5题

五、用go语言&#xff0c;考虑一个装载因子为a的开放寻址散列表。找出一个非零的a值&#xff0c;使得一次不成功查找的探查期望数是一次成功查找的探查期望数的 2 倍。这两个探查期望数可以使用定理11.6 和定理 11.8 中给定的上界。 文心一言&#xff0c;代码正常运行&#xf…
最新文章