Spring Security 小记

  前面介绍了jwt的使用,联合WebMvcConfigurer 并自定义过滤器进行token认证 https://blog.csdn.net/lileLife/article/details/104248049。现在想使用spring security结合jwt进行认证和授权。

spring security  企业应用级别的安全框架, 核心功能是用户认证Authentication和用户授权Authorization,认证就是登陆验证密码,授权就是不同用户的权限操作问题。
其实也是通过一系列过滤filter进行认证和授权。

官网:https://spring.io/projects/spring-security 

说实话,spring security真的内容超多,功能也很强大,我根据自己的理解进行下面的描述。安全框架这里还有Shiro,配置和使用就比较轻松,但功能没有本次的主角强大。对如何选择? 因为spring security需要引入的配置多 比较麻烦, 而spring boot具有自动化配置功能,所以对于spring boot来说,建议使用 spring security。 而ssm 就选择shiro

SSM+Shiro

Spring boot/SpringCloud + Spring security

Spring Security简介

从核心类开始介绍:

  • SecurityContextHolder  保存当前用户到上下文中 ,默认模式底层使用threadlocal实现。 提供对SecurityContext的访问
    存储 Authentication对象。是 secrity核心类UserDetails的一个实现类。而UserDetails对应数据库中一个用户 在SecurityContextHolder中存放类型的适配器类型。  Authentication对象也可以存放token(string类型的)
  • SecurityContext  持有Authentication对象在上下文中进行后续过滤 认证,以及授权
  • Authentication 就是认证主体了
  • GrantedAuthority 认证主体的授权,当前用户权限信息
  • UserDetails  构建认证主体 Authentication  属于核心类
  • UserDetailsService 通过username 获取UserDetails信息  
  • UsernamePasswordAuthenticationFilter ,他会封装username ,password 成Authentication 这个声明就叫做
    UsernamePasswordAuthenticationToken 也就是索  UsernamePasswordAuthenticationToken实现了 Authentication 接口 
    接着UsernamePasswordAuthenticationToken会被传递到AuthenticationManager 进行相关的校验认账
  • AuthenticationManager  进行验证,其实现类ProviderManager,AuthenticationManager中包含多个AuthenticationProvider
  • AuthenticationProvider  验证的核心工作者,也就是做认证工作

AuthenticationProvider  会使用authenticate()方法进行认证,这里manager如何处理provider对应哪个authertication呢, 其实是provider内包含两个方法,一个就是使用authenticate()认证方法,一个就是  boolean  supports(Class<?> authertication)方法,后者就是查看验证协议是否合适支持的。
AuthenticationManager  会先调动supports方法,查看是否合适,返回true则可以用该AuthenticationProvider认证。
UsernamePasswordAuthenticationToken 其实对应的是DaoAuthenticationProvider 。

(求你画个图吧,这谁能明白)

 

从上面的介绍可以大致流程。

 

Spring security小试牛刀

咱们现在通过代码进行理解:

  •  mvn引入
<!--        添加 spring security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
  •  编写UserInfo类,该类实现UserDetails,前面提到过,userDetails是核心类,后面需要用它构成认证主体 authentication
package domain;

import com.alibaba.fastjson.annotation.JSONField;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @ClassName UserInfo
 * @Description 实现 UserDetails 核心类
 * @Author lile
 * @Date 2020/2/15 14:43
 * @Version 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo implements UserDetails {

    private String token; //token
    private Integer id;
    private String phone;
    private List<String> roles; //用户权限 User Admin
    private Integer isApp; //登录为app登录  0是,1否
    
     public UserInfo(User user){
        this.id = user.getId();
        this.phone = user.getPhone();
        // 数据库没有建立 Roles规则表,这里统一权限为User
        this.roles = Arrays.asList("ROLE_USER");
    }

    
    @JSONField(serialize = false)
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }



    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getUsername() {
        return null;
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }
}
  • 编写UserInfoService

该service 实现UserDetailsService  编写通过username (此处用唯一的电话号,代替,后续token也使用电话号生成 )获取UserINfo

package com.lile.service;

import com.lile.common.mybits.model.User;
import domain.UserInfo;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * @ClassName UserInfoService
 * @Description  编写通过username (此处用唯一的电话号,代替,后续token也使用电话号生成 )获取UserINfo
 *
 *
 * @Author lile
 * @Date 2020/2/16 12:51
 * @Version 1.0
 */
@Service
public class UserInfoService implements UserDetailsService {
    @Resource
    private UserService userService;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = userService.findUserByphone(s);
        return new UserInfo(user);
    }
}

 

  • 编写认证过滤器  JwtAuthenticationTokenFilter,
    因为这里是联合 jwt token使用,所以会从requet中获取token,并进行token的验证,验证通过会获取user信息;
    接着,该过滤器通过UserInfService 从request 获取UserInfo,构成认证主体authentication 并放置在spring security 上下文中。
package com.lile.handler;

import com.lile.service.UserInfoService;
import domain.UserInfo;
import exceptions.UncheckedException;
import io.jsonwebtoken.JwtException;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import utils.ErrorCode;
import utils.JwtTokenUtil;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;

/**
 * @ClassName JwtAuthenticationTokenFilter
 * @Description 使用jwt进行 认证token,并获取当前登录用户userInfo
 * @Author lile
 * @Date 2020/2/16 13:12
 * @Version 1.0
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Resource
    private UserInfoService userInfoService;
    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        System.out.println("过滤。。。。。");
        //获取token
        String token = request.getHeader("token");
        String phone;
        if(token!=null){
            try {
                phone = jwtTokenUtil.getUsernameFromToken(token);
                if(phone!=null&&phone!=""){
                    UserInfo userInfo = (UserInfo)userInfoService.loadUserByUsername(phone);
                    // 验证 token
                    if(jwtTokenUtil.isTokenExpired(token)&& jwtTokenUtil.generateTokenByUsername(phone).equals(token)){
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                                userInfo, null, userInfo.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
                                request));
                        // 全局存放 认证对象
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }

            } catch (JwtException j) {
//                throw new UncheckedException(ErrorCode.PERMISSION_DENIED,"token错误,权限不足");
            }

        }
        filterChain.doFilter(request,response);
    }
    /// 后面重构上面的俄罗斯套娃
    private UserInfo getUserInfobyT(String token){
          String phone ="";
          phone = jwtTokenUtil.getUsernameFromToken(token);

         UserInfo userInfo = (UserInfo) userInfoService.loadUserByUsername(phone);
         return userInfo;
    }
}
  • 构建安全配置类 WebSecurityConfig , 继承WebSecurityConfigurerAdapter 。重写方法,定义相关接口url所需要的权限,并添加上面的过滤器 
package com.lile.handler;

import com.lile.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsUtils;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @ClassName WebSecurityConfig
 * @Description 安全配置类
 * @Author lile
 * @Date 2020/2/16 16:34
 * @Version 1.0
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    // Spring会自动寻找同样类型的具体类注入,这里就是JwtUserDetailsServiceImpl了
    @Resource
    private UserInfoService userInfoService;


    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userInfoService).passwordEncoder(passwordEncoderBean());
    }
    @Bean
    public PasswordEncoder passwordEncoderBean() {
        return new BCryptPasswordEncoder();
    }

//    @Autowired
//    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
//        authenticationManagerBuilder
//                // 设置UserDetailsService
//                .userDetailsService(this.userInfoService)
//                // 使用BCrypt进行密码的hash
//                .passwordEncoder(passwordEncoder());
//    }
//    // 装载BCrypt密码编码器
//    @Bean
//    public PasswordEncoder passwordEncoder() {
//        return new BCryptPasswordEncoder();
//    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        System.out.println("安全配置。。");
        httpSecurity
                //禁用CSRF保护
                .csrf().disable()
                .authorizeRequests()
                // 放行option请求
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                // 对于获取token的rest api要允许匿名访问
                .antMatchers("/hello").permitAll()
                .mvcMatchers("/v2/**").permitAll()
                //.mvcMatchers("/users/**").permitAll()
                .mvcMatchers("/hello/**").permitAll()
                .mvcMatchers("/swagger-resources/**").permitAll()
                .mvcMatchers("/swagger-ui.html#!/**").permitAll()
                //跨域 post请求两次,第一次预检请求,method 为 OPTIONS  。
                .antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
                //任何访问都必须授权
                .anyRequest().authenticated()
                //配置那些路径可以不用权限访问
                .and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .formLogin()
                //登陆成功后的处理,因为是API的形式所以不用跳转页面
                .successHandler(new RestAuthenticationSuccessHandler())
                //登陆失败后的处理
                .failureHandler(new SimpleUrlAuthenticationFailureHandler())
                .and()
                //登出后的处理
                .logout().logoutSuccessHandler(new RestLogoutSuccessHandler())
                .and()
                //认证不通过后的处理
                .exceptionHandling()
                .authenticationEntryPoint(new RestAuthenticationEntryPoint());

        // 禁用缓存
        httpSecurity.headers().cacheControl();
        // 添加Filter 如果不添加 会禁止Filter执行   使用filter过滤token
        httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
    }


    @Bean
    public JwtAuthenticationTokenFilter authenticationTokenFilterBean() {
        return new JwtAuthenticationTokenFilter();
    }


    /**
     * 登陆成功后的处理
     */
    public static class RestAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

        @Override
        public void onAuthenticationSuccess(HttpServletRequest request,
                                            HttpServletResponse response, Authentication authentication) {
            clearAuthenticationAttributes(request);
        }
    }

    /**
     * 登出成功后的处理
     */
    public static class RestLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {

        @Override
        public void onLogoutSuccess(HttpServletRequest request,
                                    HttpServletResponse response, Authentication authentication) {
        }
    }

    /**
     * 权限不通过的处理
     */
    public static class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

        @Override
        public void commence(HttpServletRequest request,
                             HttpServletResponse response,
                             AuthenticationException authException) throws IOException {
            //返回json形式的错误信息
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json");
            //设置允许跨域的配置
            // 这里填写你允许进行跨域的主机ip(正式上线时可以动态配置具体允许的域名和IP)
            response.setHeader("Access-Control-Allow-Origin", "*");
            // 允许的访问方法
            response.setHeader("Access-Control-Allow-Methods", "*");
            // Access-Control-Max-Age 用于 CORS 相关配置的缓存
            response.setHeader("Access-Control-Max-Age", "3600");
            response.setHeader("Access-Control-Allow-Headers", "*");

            response.getWriter().println("{\"code\": -2,\"msg\": \"登陆状态已过期\"}");
            response.getWriter().flush();
        }
    }
}

配置完成 ,使用:@PreAuthorize("hasRole('User')")   表示user权限 

@Api(value = "/user", description = "用户")
@RequestMapping("users")
@RestController
@Slf4j
@PreAuthorize("hasRole('USER')")
public class UserController {
	@Resource
	private UserService userService;
  • 启动项目, 控制台生成用户密码: 

每次启动都不一样,用户名默认 user  

自定义用户名密码:

配置文件  

#security
spring.security.user.name=lile
spring.security.user.password=shuaige
打印security日志的配置
#logging
logging.level.org.springframework.data:DEBUG
logging.level.org.springframework.security:DEBUG
  • 使用wagger 试验一下:

用上面的用户名密码登录  

这里登录后,发现swagger空白页面,搞什么鬼, 这个时候不能掉链子, f12后发现 swagger resource 404,后面查找发现,自己使用了WebMvcConfigurer添加了 过滤器,过滤器应该设置不包括 swagger相关url:


            @Override
            public void addInterceptors(InterceptorRegistry registry) { // 添加拦截器
                registry.addInterceptor(authenticationInterceptor())
                        .addPathPatterns("/**")
                        .excludePathPatterns("/user/login")
                        .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");

            }

            

           @Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/**")
            .addResourceLocations("classpath:/static/");

    registry.addResourceHandler("swagger-ui.html")
            .addResourceLocations("classpath:/META-INF/resources/");

    registry.addResourceHandler("/webjars/**")
           .addResourceLocations("classpath:/META-INF/resources/webjars/");
}

 

再使用 postman尝试一下: /hello  接口

因为configure 那里规则为 

.antMatchers("/hello").permitAll()

所以 /hello接口可以无权限访问。

试一下 /user/1  接口 (获取id 为1 的用户信息)

显示登出,因为该接口请求没有token信息 ,上面对权限不够进行了一次封装:

RestAuthenticationEntryPoint 

看下后台

 

  

  • 使用token,  这里需要在登录时候 返回token 给前端,前端请求时候,请求头添加 authorization:token 
	//  处理token 封装res
			String token = jwtTokenUtil.generateTokenByUsername(loginRequest.getPhone());
			UserInfo userInfo = new UserInfo(user);
			userInfo.setToken(token);
			return  userInfo;

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值