Spring security Demo

本篇文章介绍如何在项目中引入Spring security,以及一些伪代码

1、先引入Jar包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2、当我们引入上面的jar包,spring security就会启动默认配置,包括登录页面,登录账号密码,拦截路径,成功或者失败返回的内容都配置好了,如果我们实现自定义的,就要修改他的配置。先把spring security默认配置都给他换了

@Slf4j
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //认证成功后处理的bean
	@Autowired
	private AuthenticationSuccessHandler authenticationSuccessHandler;
	//认证失败处理的bean
	@Autowired
	private AuthenticationFailureHandler authenticationFailureHandler;
	//退出登录成功处理的bean
	@Autowired
	private LogoutSuccessHandler logoutSuccessHandler;
	//未登录处理
	@Autowired
	private AuthenticationEntryPoint authenticationEntryPoint;
	//用户获取用户信息的bean
	@Autowired
	private UserDetailsService userDetailsService;
	//token处理的bean,为了要有这个,因为当用户登录成功之后,每次请求只带token我们要知道谁谁
	@Autowired
	private TokenFilter tokenFilter;

	@Bean
	public BCryptPasswordEncoder bCryptPasswordEncoder() {
		//加密类
		return new BCryptPasswordEncoder();
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		log.info("用户登录拦截");
		http.csrf().disable().cors();//CSRF跨站请求伪造

		http.authorizeRequests()
				.antMatchers("/health").permitAll()
				.antMatchers("/sysUserMenu/**").hasAnyAuthority("ROLE_ADMIN")
				//需要对外暴露哪些接口可以在这里设置
				.anyRequest().authenticated();

		//解决不允许显示在iframe的问题
		http.headers().frameOptions().disable();
		http.headers().cacheControl();
		// 基于token,所以不需要session
		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

		//用来解决匿名用户访问无权限资源时的异常
		http.formLogin().and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
		//用来解决认证过的用户访问无权限资源时的异常
		//http.formLogin().and().exceptionHandling().accessDeniedHandler(new CustomAccessDeineHandler());

		//退出登录处理handler
		http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);

		//下面2行代码的作用是什么都是向filter里面添加过滤器,但是添加的filter类型各不一样。
		//前置过滤器,所有的请求一进来,先进到这个过滤器,UsernamePasswordAuthenticationFilter也就是spirng security第一个过滤器
		http.addFilterAt(tokenFilter, UsernamePasswordAuthenticationFilter.class);

		//后置过滤器,这个包含,处理成功,处理失败,包括账户认证
		http.addFilterBefore(usernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
	}

	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		AuthenticationManager manager = super.authenticationManagerBean();
		return manager;
	}

	@Bean
	public MyAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
		MyAuthenticationFilter filter = new MyAuthenticationFilter();
		filter.setAuthenticationManager(authenticationManagerBean());//添加AuthenticationManager账户认证,这个类很重要。
		filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);//成功处理器
		filter.setAuthenticationFailureHandler(authenticationFailureHandler);//失败处理器
		return filter;
	}
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService)//配置UsernamePasswordAuthenticationFitel过滤器
			.passwordEncoder(bCryptPasswordEncoder());//配置passwordEncoder用于密码的加密和比对,认证用户,认证用户的密码和数据是否一致
	}
}

上面代码做的几个事情
1、配置那些请求需要拦截,那些不需要拦截
2、往Spring Security拦截器链中加入自定义的拦截器(加入了2个拦截器MyAuthenticationFilter、TokenFilter)
3、在拦截器中配置认证成功,认证失败的处理逻辑。
4、配置获取用户信息处理的bean UserDetailService

备注:上面2个拦截器分别是在首次登录通过处理前端穿的username和password来做处理,第二个是拦截器是在后续登录只传token来做处理。

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		//通过username得到对象,查数据库也好,读配置也好
		UserDetails userDetails = new UserDetails();
		//设置用户权限
		userDetails.setRoles(roles);
		return userDetails;
	}
}

实现UserDetailsService用来获取UserDetails,为什么要这样做。因为Spring Security里面有个AuthenticationManager他会根据前端穿的密码和从数据库通过username查到的密码进行对比认证。下面引入一段源码。

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

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
	for (AuthenticationProvider provider : getProviders()) {
		if (!provider.supports(toTest)) {
			continue;
		}
		result = provider.authenticate(authentication);
		if (result != null) {
			copyDetails(authentication, result);
			break;
		}
	}
	if (result == null && parent != null) {
		result = parentResult = parent.authenticate(authentication);
	}
}

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
	user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
	preAuthenticationChecks.check(user);
	additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);

	postAuthenticationChecks.check(user);
	return createSuccessAuthentication(principalToReturn, authentication, user);
}

protected final UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//重点在这里会调用我们实现的UserDetailsService
	UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}

下面引入第一个过滤器TokenFilter,主要是在用户已经登录成功之后,后续在请求进来,那么穿的就不是账号密码而是token。这个时候我们就要把token转成UserDetails就要用到这个过滤器。

@Component
public class TokenFilter  extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        //获取token
        String token = this.getToken(request);
        logger.info("用户校验token"+token);
        if (StringUtils.isNotBlank(token)) {
            //通过token的到用户信息,可以基于Redis来做
            UserDetails userDetails = getUserDetailsByToken(token);
            if (userDetails != null) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
                        null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(request, response);
    }
}

下面是第二个过滤器,主要用于从请求中获取username和password,然后把他转成UsernamePasswordAuthenticationToken交给AuthenticationManager来认证。

public class MyAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public MyAuthenticationFilter() {
        super(new AntPathRequestMatcher("/xtapi/users/login", "POST"));
    }

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //判断请求方式为POST
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("xxx");
        }

        //获取请求参数类型
        String contentType = request.getContentType();
        if(!contentType.equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
                && !contentType.equals(MediaType.APPLICATION_JSON_VALUE)){
            //如果请求类型是非postJson方式
            throw new AuthenticationServiceException("xxx");
        }

        //认证凭证对象
        UsernamePasswordAuthenticationToken authRequest = null;
        try (InputStream is = request.getInputStream()){
            ObjectMapper mapper = new ObjectMapper();
            Map<String,String> map = mapper.readValue(is, Map.class);
            //得到自定义的参数
            authRequest = new UsernamePasswordAuthenticationToken(map.get("userName"),map.get("password"));
        }catch (Exception e){
            authRequest = new UsernamePasswordAuthenticationToken("","");
        }finally {
            setDetails(request,authRequest);
            //开始认证
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
}

下面是认证成功或者认证失败的处理bean

@Configuration
public class SecurityHandlerConfig {
	/**
	 * 登陆成功,返回Token
	 */
	@Bean
	public AuthenticationSuccessHandler loginSuccessHandler() {
		return new AuthenticationSuccessHandler() {
			@Override
			public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                                Authentication authentication) throws IOException, ServletException {
				UserDetails userDetails = (UserDetails) authentication.getPrincipal();
				Map<String,Object> map = new HashMap<>();
				//通过userDetails生成token并把token存入缓存中
				Token token = saveToken(userDetails);
				map.put("token",token.getToken());
				ResponseUtil.responseJson(request,response, HttpStatus.OK.value(), map);
			}
		};
	}

	/**
	 * 登陆失败
	 */
	@Bean
	public AuthenticationFailureHandler loginFailureHandler() {
		return new AuthenticationFailureHandler() {
			@Override
			public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                                AuthenticationException exception) throws IOException, ServletException {
				String msg = null;
				if (exception instanceof BadCredentialsException) {
					msg = "密码错误";
				} else {
					msg = exception.getMessage();
				}
				ResponseInfo info = new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", msg);
				ResponseUtil.responseJson(request,response, HttpStatus.UNAUTHORIZED.value(), info);
			}
		};
	}

	/**
	 * 未登录,返回401
	 */
	@Bean
	public AuthenticationEntryPoint authenticationEntryPoint() {
		return new AuthenticationEntryPoint() {
			@Override
			public void commence(HttpServletRequest request, HttpServletResponse response,
                                 AuthenticationException authException) throws IOException, ServletException {
				ResponseInfo info = new ResponseInfo(HttpStatus.FORBIDDEN.value() + "", "请先登录");
				ResponseUtil.responseJson(request,response, HttpStatus.FORBIDDEN.value(), info);
			}
		};
	}

	@Bean
	public AccessDeniedHandler accessDeniedHandler(){
		return new AccessDeniedHandler() {
			@Override
			public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
				ResponseInfo info = new ResponseInfo(HttpStatus.FORBIDDEN.value() + "", "没有访问权限!");
				ResponseUtil.responseJson(request,response, HttpStatus.FORBIDDEN.value(), info);
			}
		};
	}

	/**
	 * 退出处理
	 */
	@Bean
	public LogoutSuccessHandler logoutSussHandler() {
		return new LogoutSuccessHandler() {
			@Override
			public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
				ResponseInfo info = new ResponseInfo(HttpStatus.OK.value() + "", "退出成功");

				//通过token得到得到缓存
				String token = getUserDetailsByToken(request);
				//讲token从缓存中移除
				this.deleteToken(token);
				ResponseUtil.responseJson(request,response, HttpStatus.OK.value(), info);
			}
		};
	}
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

信仰_273993243

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值