spring boot 集成 security +jwt 认证授权

前言

最近在写登录、注册接口,所以就写了这个demo来练练手,实际开发还是需要根据不同环境做调整
鄙人记性差,写这个正好回顾一下 ,网上找了很多资料,还是谢谢各路大神的指引
已经自测过,登录和权限认证都没问题

spring boot 集成security

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.78</version>
        </dependency>

最基础的导包
连接数据库,配置jdbc,mybatisplus这些我就不多说了

1. 认证流程

首先网上抄个JWTUtil token生成工具,然后配置redis(用于存储token)这里就随意了
认证流程我就不走一遍了,有兴趣的网上搜一搜,贴点主要的
首先进入UsernamePasswordAuthenticationFilter

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		String username = obtainUsername(request);
		username = (username != null) ? username : "";
		username = username.trim();
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}

注意的是,这里是表单提交才能获取到username和password,我被坑了好久才找到原因
而且提交参数必须和这个一致
然后进入this.getAuthenticationManager().authenticate(authRequest);

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
	for (AuthenticationProvider provider : getProviders()) {
		.......篇幅太长就省略了
		result = provider.authenticate(authentication);
		return result;
		}
	}
}

这里交给了AuthenticationProvider的实现类 来处理
大概意思就是多种认证方式,只要有一个成功了,就放入Authentication中去
后面的手机号码登录、第三方登录会用到
第二个是不是特别很眼熟
AbstractUserDetailsAuthenticationProvider

public abstract class AbstractUserDetailsAuthenticationProvider implements 	AuthenticationProvider{
	protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
}

这是一个抽象方法,又交给它的子类(工具人)来干实事
DaoAuthenticationProvider

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Override
	@SuppressWarnings("deprecation")
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			this.logger.debug("Failed to authenticate since no credentials provided");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
		String presentedPassword = authentication.getCredentials().toString();
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			this.logger.debug("Failed to authenticate since password does not match stored value");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}
	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
}

2.认证开始

additionalAuthenticationChecks方法中两个参数UserDetails 是我们从数据库查询出来的用户数据
UsernamePasswordAuthenticationToken 是用户填写的时提交的账户密码,交给security封装好的
这里就比对一下密码是否一样,当然账户比对在前面已经做了。
那么首先就是创建一个UserDetails 来封装我们从数据库里查到的用户信息

public class LoginUser implements UserDetails {
		//存储用户信息
    private User user;
    //存储 securityContext所需权限的集合
    private List<SimpleGrantedAuthority> authorities;
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

然后就是从哪里获取这些值,请注意

this.getUserDetailsService().loadUserByUsername(username);

所以需要实现UserDetailsService

@Service
public class UserDetailServiceImp implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("username:{}",username);
        User user = userRepository.selectByUserName(username);
        if(Objects.isNull(user))
            throw new UsernameNotFoundException("用户名或密码错误");
        LoginUser loginUser = new LoginUser();
        loginUser.setUser(user);
        return loginUser;
    }
}

经过过滤链,如果认证成功,那么你可以在Security上下文中获取到用户信息
在自己的登录service中获取

	//这里会在config配置里注入
    @Autowired
    private AuthenticationManager authenticationManager;
    
	Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		//如果不用表单提交,则可以用这种方式
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(username, password);
       
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        
		//认证通过,生成一个token
        LoginUser principal = (LoginUser) authentication.getPrincipal();
        User user = principal.getUser();
        String userId = user.getUserId();
        String token = JWTUtil.createToken(userId);
        //把user放入redis中
        redisUtil.set(userId,user,60*60);
        return   JsonResult.success(token);

最后就是jwt过滤

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
 @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("base_token");
        if(StringUtils.isEmpty(token)){
            //如果token为空,放行,给security拦截,因为有可能是登录
            filterChain.doFilter(request,response);
            return;
        }
        //解析token
        Claims claims = JWTUtil.parseToken(token);
        String userId = claims.getSubject();
        //从数据库获取到user信息
        User user = (User)redisUtil.get(userId);
        if(Objects.isNull(user)){
            throw  new UsernameNotFoundException("用户未登录");
        }
        //第三个参数是权限,后面鉴权会加上去
         UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user,null,null);

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        //放行
        filterChain.doFilter(request,response);
        }
}

最后就是配置securityConfig

@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    private UserDetailServiceImp userDetailServiceImp;
    
    @Bean
    protected PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
		 @Override
    protected void configure(HttpSecurity http) throws Exception {
        //放行登录接口 user/login   anonymous()只能未登录的状态访问
        http.authorizeRequests()
                .antMatchers("/login").anonymous()
                //permitAll() 所有人能访问(登录和未登录)
                .antMatchers("/index").permitAll()
                //除上述外所有请求都要认证
                .and().authorizeRequests();
               
        //基于表单提交的login会进入UsernamePasswordAuthenticationFilter
        //http.formLogin().loginPage("/login");
        //csrf  关闭session获取到securityContext
        http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //注销 并回到首页
        http.logout().logoutUrl("/");
        //记住我功能
        http.rememberMe();
        //把自定义的jwt过滤器 放在 UsernamePasswordAuthenticationFilter 之前执行
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }
    //认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userDetailServiceImp);
    }


    @Override
    public void configure(WebSecurity web) throws Exception {
        //放行所需要用到的静态资源,允许访问 swagger相关
        web.ignoring().antMatchers( "/swagger-ui.html",
                "/swagger-ui/*",
                "/swagger-resources/**",
                "/v2/api-docs",
                "/v3/api-docs",
                "/webjars/**");
    }
}

这里使用的BCryptPasswordEncoder,所以在设置密码的时候也要用这个来编码
下次有空再把授权补上吧

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值