SpringSecurity实现Jwt自定义登录

一、springsecurity的工作流程

上手之前先看一下springsecurity的工作流程:
Java设计模式中有一种责任链模式,而spring security就是基于这种模式,利用多个过滤器Filter,组成一条过滤链,针对web请求进行安全验证,同时会将验证的信息封装成身份令牌:Authentication,在访问受保护的资源时,通过来校验上下文中的Authentication来判断是否允许访问。

基于前后端分离的情况下,对于spring security在项目中的使用,一般都是后端提供登录接口给到前端,当用户登录成功返回token或者cookie,前端拿着令牌访问后端资源。

再来认识一些重要的类:
Authentication:认证通过构建的身份令牌

public interface Authentication extends Principal, Serializable {
	//角色权限标识集合
    Collection<? extends GrantedAuthority> getAuthorities();
	//一般存放我们的密码
    Object getCredentials();
	//一般存放用户信息
    Object getDetails();
	//获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails
    Object getPrincipal();
	//是否认证 true false
    boolean isAuthenticated();
	//设置isAuthenticated
    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

Authentication是一个接口,下面还有不同的具体实现;

SecurityContext:上下文对象,我们的上面讲到的令牌Authentication就是放在这个里面:

public interface SecurityContext extends Serializable {
	//只有俩个方法,get/set
    Authentication getAuthentication();

    void setAuthentication(Authentication var1);
}

SecurityContextHolder:获取SecurityContext的静态工具类
AuthenticationManager:内部的authenticate方法针对web请求进行认证,放回认证的Authentication对象。

简单描述下springsecurity的认证流程就是:
1、将请求丢给AuthenticationManagerauthenticate方法进行认证
2、认证成功后返回Authentication对象
3、SecurityContextHolder获取SecurityContext对象
4、将Authentication放入SecurityContext

二、springboot+springsecurity+jwt

方案:对于前段后分离的情况下,再加上微服务,cookie-session 的方案可能不太友好,除非服务端将session做成服务节点共享,放到缓存等等,除此以外jwt令牌、或者token+redis也能在微服务中作为登录方案,后端提供登录接口给到前端,认证成功则返回jwt令牌,后续请求所有接口前端都要携带上jwt令牌,后端接受到请求需要在springsecurity中添加filter对令牌做解析,放行请求。

1、项目准备

pom.xml

 <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.1.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>

        <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>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>

    </dependencies>

2、编写过滤器AuthenticationLoginFilter

public class AuthenticationLoginFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * 构造方法,调用父类的,设置登录地址/login,请求方式POST
     */
    public AuthenticationLoginFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
        //获取表单提交数据
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        //封装到token中提交
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
        return getAuthenticationManager().authenticate(authRequest);

    }
}

这个过滤器相当于我们定义了登录接口,AbstractAuthenticationProcessingFilter本身有了一个子类UsernamePasswordAuthenticationFilter继承,我们这里选择重写attemptAuthentication方法,可以让我们直接访问到/login接口,我们再来看一下springsecurity内部UsernamePasswordAuthenticationFilter本身的实现:

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {
		
	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
	private boolean postOnly = true;

	public UsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login", "POST"));
	}
	
	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}
		if (password == null) {
			password = "";
		}
		username = username.trim();

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}	
}

和我们AuthenticationLoginFilter没什么区别,在springsecurity配置中,需要将我们写的AuthenticationLoginFilter 添加到UsernamePasswordAuthenticationFilter的前面,并且在AuthenticationLoginFilter 注入其需要的bean。

3、编写登录成功/失败处理器

在认证成功或者认证失败,都会调用相对应的处理器执行成功/失败方法

1、登录成功处理器LoginAuthenticationSuccessHandler

@Component
@Slf4j
public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtUtils jwtUtils;
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
                                        HttpServletResponse httpServletResponse,
                                        Authentication authentication) throws IOException {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        SecurityContextHolder.getContext().setAuthentication(authentication);
        //生成令牌
        String accessToken = jwtUtils.createToken(userDetails.getUsername());
        renderToken(httpServletResponse, LoginToken.builder().accessToken(accessToken).build());
    }

    /**
     * 渲染返回 token 数据,因为前端页面接收的都是Result对象,故使用application/json返回
     */
    public void renderToken(HttpServletResponse response, LoginToken token) throws IOException {
        ResponseUtils.result(response, R.ok(token, "登录成功"));
    }
}

工具类 ResponseUtils 和 响应体:

public class ResponseUtils {

    public static void result(HttpServletResponse response, R msg) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream out = response.getOutputStream();
        ObjectMapper objectMapper = new ObjectMapper();
        out.write(objectMapper.writeValueAsString(msg).getBytes("UTF-8"));
        out.flush();
        out.close();
    }
}

返回响应体

@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class R<T> implements Serializable {

	private static final long serialVersionUID = 1L;

	@Getter
	@Setter
	/**
	 * 返回标记:成功标记=0,失败标记=1
	 */
	private int code;

	@Getter
	@Setter
	/**
	 * 返回信息
	 */
	private String msg;

	@Getter
	@Setter
	/**
	 * 数据
	 */
	private T data;

	public static <T> R<T> ok() {
		return restResult(null, CommonConstants.SUCCESS, null);
	}

	public static <T> R<T> ok(T data) {
		return restResult(data, CommonConstants.SUCCESS, null);
	}

	public static <T> R<T> ok(T data, String msg) {
		return restResult(data, CommonConstants.SUCCESS, msg);
	}

	public static <T> R<T> failed() {
		return restResult(null, CommonConstants.FAIL, null);
	}

	public static <T> R<T> failed(String msg) {
		return restResult(null, CommonConstants.FAIL, msg);
	}

	public static <T> R<T> failed(T data) {
		return restResult(data, CommonConstants.FAIL, null);
	}

	public static <T> R<T> failed(T data, String msg) {
		return restResult(data, CommonConstants.FAIL, msg);
	}

	private static <T> R<T> restResult(T data, int code, String msg) {
		R<T> apiResult = new R<>();
		apiResult.setCode(code);
		apiResult.setData(data);
		apiResult.setMsg(msg);
		return apiResult;
	}

}
public interface CommonConstants {
	
	/**
	 * 成功标记
	 */
	Integer SUCCESS = 0;

	/**
	 * 失败标记
	 */
	Integer FAIL = 1;
	
}

2、登录失败处理器LoginAuthenticationFailureHandler

@Component
public class LoginAuthenticationFailureHandler implements AuthenticationFailureHandler {
    /**
     * 一旦登录失败则会被调用
     *
     * @param httpServletRequest
     * @param response
     * @param exception          这个参数是异常信息,可以根据不同的异常类返回不同的提示信息
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {

        //TODO 根据项目需要返回指定异常提示,陈某这里演示了一个用户名密码错误的异常
        //BadCredentialsException 这个异常一般是用户名或者密码错误
        if (exception instanceof BadCredentialsException) {
            ResponseUtils.result(response, R.failed("用户名或密码不正确"));
        }
        ResponseUtils.result(response, R.failed("登录失败"));
    }
}

4、实现UserDetailsService

UserDetailsService主要就是为了那用户名称去我们的数据库查询,这里我们需要自己去实现再注入到springsecurity
UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if ("root".equals(username)){
            SecurityUser userDetails = new SecurityUser();
            userDetails.setUsername("root");
            userDetails.setPassword(passwordEncoder.encode("root"));
            //角色
            SimpleGrantedAuthority grantedAuthorityRole = new SimpleGrantedAuthority("ROLE_ADMIN");
           
            List<GrantedAuthority> list = new ArrayList<>();
            list.add(grantedAuthorityRole);
           
            userDetails.setAuthorities(list);
            return userDetails;
        }
        throw new UsernameNotFoundException("用户不存在");
    }
    
}

因为这里需要连接数据库,为了节省时间,直接写死 root 账号。

5、权限不足和未登录访问资源处理器

1、权限不足处理器RequestAccessDeniedHandler

@Component
public class RequestAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException {
        ResponseUtils.result(response, R.failed("权限不足"));
    }
}

2、未登录访问资源处理器EntryPointUnauthorizedHandler

@Component
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        ResponseUtils.result(response,R.failed("无权限,请先登录"));
    }
}

6、Jwt验证过滤器TokenAuthenticationFilter

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    /**
     * UserDetailsService的实现类,从数据库中加载用户详细信息
     */
    @Qualifier("userDetailsServiceImpl")
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String token = request.getHeader("token");
        /**
         * token存在则校验token
         * 1. token是否存在
         * 2. token存在:
         *  2.1 校验token中的用户名是否失效
         */
        if (!StringUtils.isEmpty(token)){
            String username = jwtUtils.getUsernameFromToken(token);
            //SecurityContextHolder.getContext().getAuthentication()==null 未认证则为true
            if (!StringUtils.isEmpty(username) && SecurityContextHolder.getContext().getAuthentication()==null){
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                //如果token有效
                if ("root".equals(userDetails.getUsername())){
                    // 将用户信息存入 authentication,方便后续校验
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null,
                            userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    // 将 authentication 存入 ThreadLocal,方便后续获取用户信息
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        //继续执行下一个过滤器
        chain.doFilter(request,response);
    }
}

7、springsecurity配置

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;

    @Autowired
    private RequestAccessDeniedHandler requestAccessDeniedHandler;

    /**
     * 登录成功处理器
     */
    @Autowired
    private LoginAuthenticationSuccessHandler loginAuthenticationSuccessHandler;

    /**
     * 登录失败处理器
     */
    @Autowired
    private LoginAuthenticationFailureHandler loginAuthenticationFailureHandler;

    /**
     * 加密
     */
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * userDetailService
     */
    @Qualifier("userDetailsServiceImpl")
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        AuthenticationLoginFilter filter = new AuthenticationLoginFilter();
        filter.setAuthenticationManager(authenticationManager);
        //认证成功处理器
        filter.setAuthenticationSuccessHandler(loginAuthenticationSuccessHandler);
        //认证失败处理器
        filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);
        //将这个过滤器添加到UsernamePasswordAuthenticationFilter之前执行
        http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);

        //直接使用DaoAuthenticationProvider
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        //设置userDetailService
        provider.setUserDetailsService(userDetailsService);
        //设置加密算法
        provider.setPasswordEncoder(passwordEncoder);
        http.authenticationProvider(provider);

        http.formLogin()
                //禁用表单登录,前后端分离用不上
                .disable()
                //应用登录过滤器的配置,配置分离
                // 设置URL的授权
                .authorizeRequests()
                // 这里需要将登录页面放行,permitAll()表示不再拦截,/login 登录的url
                .antMatchers("/login")
                .permitAll()
    
                // anyRequest() 所有请求   authenticated() 必须被认证
                .anyRequest()
                .authenticated()
                //处理异常情况:认证失败和权限不足
                .and()
                .exceptionHandling()
                //认证未通过,不允许访问异常处理器
                .authenticationEntryPoint(entryPointUnauthorizedHandler)
                //认证通过,但是没权限处理器
                .accessDeniedHandler(requestAccessDeniedHandler)
                .and()
                //禁用session,JWT校验不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                //将TOKEN校验过滤器配置到过滤器链中,否则不生效,放到UsernamePasswordAuthenticationFilter之前
                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class)
                // 关闭csrf
                .csrf().disable();

    }

    // 自定义的Jwt Token校验过滤器
    @Bean
    public TokenAuthenticationFilter authenticationTokenFilterBean() {
        return new TokenAuthenticationFilter();
    }

    /**
     * 加密算法
     *
     * @return
     */
    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
}

8、接口测试

登录接口

1、登录成功 ip:端口/login
username:root
password:root
在这里插入图片描述

2、登录失败
username:root
password:321456(错误密码)
在这里插入图片描述

携带令牌访问受保护的资源

定义一个UserController

@RestController
@RequestMapping("/user")
public class UserController {


    @GetMapping()
    //只有角色为'ROLE_ADMIN'
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public String getUser() throws JsonProcessingException {
        return "root";
    }

    @GetMapping("/info")
    //只有角色'ROLE_USER'
    @PreAuthorize("hasRole('ROLE_USER')")
    public String getUserInfo() throws JsonProcessingException {
        return "root";
    }
}

将登陆返回的access_token 携带在头部进行请求
在这里插入图片描述
访问/user/info

在这里插入图片描述
因为我们代码中赋值的角色信息为’ROLE_ADMIN’,所以访问/use成功,访问/user/info 则显示权限不足

当我们不携带token访问资源时:
在这里插入图片描述
提示我们需要先登录。

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Spring Security校验JWT Token的代码可以参考如下: 首先,需要创建JWT Token的验证过滤器类。该类继承自OncePerRequestFilter,并在doFilterInternal()方法中实现JWT Token的校验逻辑: ```java public class JwtTokenAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtTokenProvider jwtTokenProvider; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = jwtTokenProvider.resolveToken(request); try { if (token != null && jwtTokenProvider.validateToken(token)) { Authentication auth = jwtTokenProvider.getAuthentication(token); if (auth != null) { SecurityContextHolder.getContext().setAuthentication(auth); } } } catch (JwtException e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); return; } filterChain.doFilter(request, response); } } ``` 然后,需要创建JWT Token的提供者类,该类负责创建Token并验证Token: ```java @Component public class JwtTokenProvider { @Value("${jwt.secret}") private String secretKey; @Value("${jwt.token.validity}") private long validityInMilliseconds; private Key getSecretKey() { return Keys.hmacShaKeyFor(secretKey.getBytes()); } public String createToken(String username, List<Role> roles) { Claims claims = Jwts.claims().setSubject(username); claims.put("auth", roles.stream().map(role -> new SimpleGrantedAuthority(role.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList())); Date now = new Date(); Date validity = new Date(now.getTime() + validityInMilliseconds); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) .setExpiration(validity) .signWith(getSecretKey()) .compact(); } public Authentication getAuthentication(String token) { UserDetails userDetails = new User(getUsername(token), "", getAuthorities(token)); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } private String getUsername(String token) { return Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token).getBody().getSubject(); } private List<GrantedAuthority> getAuthorities(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(getSecretKey()) .build() .parseClaimsJws(token) .getBody(); List<LinkedHashMap<String, String>> roles = (List<LinkedHashMap<String, String>>) claims.get("auth"); return roles.stream().map(role -> new SimpleGrantedAuthority(role.get("authority"))).collect(Collectors.toList()); } public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { e.printStackTrace(); return false; } } public String resolveToken(HttpServletRequest req) { String bearerToken = req.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } ``` 最后,需要把上述过滤器类和提供者类添加到Spring Security的配置中: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtTokenProvider jwtTokenProvider; @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/api/v1/auth/login").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new JwtTokenAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder()); } @Bean public UserDetailsService userDetailsService() { return new UserServiceImpl(); } } ``` 以上代码用来实现Spring Security校验JWT Token的功能,提供了创建Token、校验Token和获取Token中存储的用户和权限信息等相关方法。您可以根据您的实际需求进行修改和调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值