springsecurityspringboot

Spring Security-整合Spring Boot(分布

2022-05-19  本文已影响0人  石头耳东

前置文章:
Spring Security-整合Spring Boot(分布式)-篇章一:主要介绍了JWT、RSA以及Security的认证校验逻辑;
Spring Security-整合Spring Boot(分布式)-篇章二:主要介绍JWT工具类,以及RSA工具类的基础使用。是一些固定的用法,可以跟据需求了解即可;

前言:本文是认证模块和资源模块的代码实现,其中【用户认证】、【身份校验】两个过滤器是关键内容。

零、本文纲要

一、security_auth_server(认证模块)
二、security_source_product(资源模块)

一、security_auth_server(认证模块)

  1. application.yml
  2. domain/entity
  3. mapper
  4. service接口及实现类
  5. filter
  6. config
  7. 启动类

0. application.yml

server:
  port: 9001
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///security_authority?serverTimezone=UTC&useSSL=false
    username: root
    password: root
mybatis:
  type-aliases-package: com.stone.domain
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.stone: debug
rsa:
  key:
    publicKeyFile: D:\tmp\auth\id_key_rsa.pub
    privateKeyFile: D:\tmp\auth\id_key_rsa.pri

1. domain/entity

GrantedAuthority是认证业务授权的接口

public class SysRole implements GrantedAuthority {

    private Integer id;
    private String roleName;
    private String roleDesc;

    /*
        此处省略对象属性的get/set方法
    */

    @JsonIgnore //此方法是规范的方法,不做序列化处理
    @Override
    public String getAuthority() {
        return roleName;
    }
}

UserDetails是认证业务的返回值

public class SysUser implements UserDetails {

    private Integer id;
    private String username;
    private String password;
    private Integer status;
    private List<SysRole> roles;

    /*
        此处省略对象属性的get/set方法
    */

    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles;
    }

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

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

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true; //账户是否失效
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true; //账户是否锁定
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true; //凭据(密码)是否失效
    }

    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true; //是否可用
    }
}

注意:此处我们把账户状态的内容写死了,具体业务如果需要灵活的状态,需要手动编写其逻辑代码。

2. mapper

public interface RoleMapper{
    @Select("select r.id, r.role_name roleName, r.role_desc roleDesc " +
            "from sys_role r, sys_user_role ur " +
            "where r.id = ur.rid and ur.uid = #{uid}")
    public List<SysRole> findById(Integer uid);
}
public interface UserMapper{
    @Select("select * from sys_user where username = #{username}")
    @Results({
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "roles", column = "id", javaType = List.class,
                    many = @Many(select = "com.stone.mapper.RoleMapper.findById"))
    })
    public SysUser findByName(String username);
}

3. service接口及实现类

public interface UserService extends UserDetailsService {
}

此处我们重写UserDetailsService接口的loadUserByUsername方法,此方法是认证业务需要使用到的。另外,因为我们的SysRole和SysUser分别实现了GrantedAuthority和UserDetails接口,所以调用mapper获取的结果即是框架所需的UserDetails实现类。

@Service
@Transactional
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userMapper.findByName(username);
    }
}

4. filter

此处我们继承UsernamePasswordAuthenticationFilter类,重写其attemptAuthentication认证用户方法,以及认证通过回写token的方法successfulAuthentication。

Ⅰ attemptAuthentication认证用户方法

原方法是接收POST请求的form表单数据。而前后端分离的开发环境下,我们更多使用AJAX请求,传递的是json字符串。所以此处做响应的代码调整。

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    private RsaKeyProperties prop;

    public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
        this.authenticationManager = authenticationManager;
        this.prop = prop;
    }

    /*
     * 认证用户的方法
     * @param request 请求对象
     * @param response 响应对象
     * @return Authentication
     * @throws AuthenticationException 认证异常
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        try {
            //从请求对象中获取SysUser对象
            SysUser sysUser = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);

            //封装为UsernamePasswordAuthenticationToken用于用户认证
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    sysUser.getUsername(),
                    sysUser.getPassword());

            //进行具体的认证
            return authenticationManager.authenticate(authRequest);
        } catch (Exception e) {
            try {
                //如果认证失败,提供自定义json格式异常
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                PrintWriter out = response.getWriter();
                Map<String, Object> map = new HashMap<String, Object>();
                map.put("code", HttpServletResponse.SC_UNAUTHORIZED);
                map.put("message", "账号或密码错误!");
                out.write(new ObjectMapper().writeValueAsString(map));
                out.flush();
                out.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            throw new RuntimeException(e);
        }
    }

    /*
     * 认证通过回写token的方法
     * @param request 请求对象
     * @param response 响应对象
     * @param chain 过滤器链
     * @param authResult 认证结果
     * @throws IOException IO异常
     * @throws ServletException Servlet异常
     */
    @Override
    public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        SysUser sysUser = new SysUser();
        sysUser.setUsername(authResult.getName());
        sysUser.setRoles((List<SysRole>) authResult.getAuthorities());
        String token = JwtUtils.generateTokenExpireInMinutes(sysUser, prop.getPrivateKey(), 24 * 60);

        /*
            此处引用了JWT官方对token的规范说明,具体如下:
            https://jwt.io/introduction/
            Whenever the user wants to access a protected route or resource, the user agent should send the JWT,
            typically in the Authorization header using the Bearer schema. The content of the header should look
            like the following:
                Authorization: Bearer <token>
        */

        //往响应头中添加token
        response.addHeader("Authorization", "Bearer " + token);

        try {
            //登录成功時,返回json格式进行提示
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_OK);
            PrintWriter out = response.getWriter();
            Map<String, Object> resultMap = new HashMap<String, Object>();
            resultMap.put("code", HttpServletResponse.SC_OK);
            resultMap.put("message", "认证通过!");
            out.write(new ObjectMapper().writeValueAsString(resultMap));
            out.flush();
            out.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

此处我们自定义JwtVerifyFilter继承token校验的filter类BasicAuthenticationFilter

public class JwtVerifyFilter extends BasicAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    private RsaKeyProperties rsaKeyProperties;

    public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
        super(authenticationManager);
        this.rsaKeyProperties = rsaKeyProperties;
    }

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        //从请求头中获取token
        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Bearer ")) {
            //如果没有携带错误的token(未有效认证的),则提示用户登录!
            chain.doFilter(request, response);
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter out = response.getWriter();
            Map<String, Object> resultMap = new HashMap<String, Object>();
            resultMap.put("code", HttpServletResponse.SC_FORBIDDEN);
            resultMap.put("msg", "请登录!");
            out.write(new ObjectMapper().writeValueAsString(resultMap));
            out.flush();
            out.close();
            return;
        }

        //如果携带了正确格式的token,首先得到token
        String token = header.replace("Bearer ", "");
        //公钥解密,验证token是否正确
        Payload<SysUser> payload = JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(), SysUser.class);
        SysUser sysUser = payload.getUserInfo();
        if (sysUser != null) {
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    sysUser.getUsername(), null, sysUser.getAuthorities());
            //往SecurityContext容器中存储授权信息
            SecurityContextHolder.getContext().setAuthentication(authRequest);
            chain.doFilter(request, response);
        }

    }
}

5. config

用于生成公钥私钥的配置类

@ConfigurationProperties("rsa.key")
public class RsaKeyProperties {
    private String publicKeyFile;
    private String privateKeyFile;

    private PublicKey publicKey;
    private PrivateKey privateKey;

    @PostConstruct //在对象加载完依赖注入后执行(Constructor > @Autowired > @PostConstruct),生成公钥私钥
    public void createRsaKey() throws Exception {
        publicKey = RsaUtils.getPublicKey(publicKeyFile);
        privateKey = RsaUtils.getPrivateKey(privateKeyFile);
    }

    /*
        此处省略对象属性的get/set方法
    */

}
@Configuration
@EnableWebSecurity //开启认证配置的支持,继承WebSecurityConfigurerAdapter重写配置
@EnableGlobalMethodSecurity(securedEnabled = true) //开启注解的支持
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Autowired
    private RsaKeyProperties rsaKeyProperties;

    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    //认证用户的来源【数据库】
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    //配置Spring Security的相关信息
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //释放静态资源,指定资源拦截规则,指定自定义认证页面,指定退出认证配置,csrf配置
        http.cors().and().csrf().disable()
                .authorizeRequests().antMatchers("/**").hasAnyRole("USER")
                .anyRequest().authenticated()
                .and()
                .addFilter(new JwtLoginFilter(authenticationManager(), rsaKeyProperties))
                .addFilter(new JwtVerifyFilter(authenticationManager(), rsaKeyProperties))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//不创建HttpSession
    }
}

6. 启动类

@SpringBootApplication
@MapperScan("com.stone.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class) //开启@ConfigurationProperties注解的支持
public class AuthServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthServerApplication.class, args);
    }
}

7. 测试

postman

POST请求:http://localhost:9001/login

Body-raw-JSON

{
    "username":"stone",
    "password":"123456"
}

此时,我们可以在响应头中获取到认证后的token

二、security_source_product(资源模块)

  1. application.yml
  2. domain/entity
  3. mapper
  4. service接口及实现类
  5. filter
  6. config
  7. controller
  8. 启动类

0. application.yml

注意:此处除了端口不同之外,重要的是需要把私钥从配置文件中去除,只配置公钥信息

server:
  port: 9002
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///security_authority?serverTimezone=UTC&useSSL=false
    username: root
    password: root
mybatis:
  type-aliases-package: com.stone.domain
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.stone: debug
rsa:
  key:
    publicKeyFile: D:\tmp\auth\id_key_rsa.pub

1. domain/entity

注意:此处的SysRole、SysUser与认证模块的一致,可以跟据项目实际需要,看是否把这些类提取到common共用模块

2. mapper

注意:由于SysRole、SysUser的关键信息是通过token解析出来的,所以此处不需要认证相关的mapper

3. service接口及实现类

public interface ProductService {
    public String findAll();
}
@Service
public class ProductServiceImpl implements ProductService {
    @Secured({"ROLE_ADMIN","ROLE_PRODUCT"})
    @Override
    public String findAll() {
        return "产品列表查询成功";
    }
}

4. filter

该类与server模块的一致。

注意:用户认证的过滤在server模块进行,此处只需要配置JwtVerifyFilter类。

5. config

注意:由于资源模块只需做身份校验,此处仅配置公钥的内容。

@ConfigurationProperties("rsa.key")
public class RsaKeyProperties {
    private String publicKeyFile;

    private PublicKey publicKey;

    @PostConstruct
    public void createRsaKey() throws Exception {
        publicKey = RsaUtils.getPublicKey(publicKeyFile);
    }

    /*
        此处省略对象属性的get/set方法
    */

}

注意:与认证模块相比,此处的配置少了密码编码类,以及认证用户的来源的配置。

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

    @Autowired
    private RsaKeyProperties rsaKeyProperties;

    //配置Spring Security的相关信息
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //释放静态资源,指定资源拦截规则,指定自定义认证页面,指定退出认证配置,csrf配置
        http.cors().and().csrf().disable()
                .authorizeRequests().antMatchers("/product").hasAnyRole("PRODUCT")
                .anyRequest().authenticated()
                .and()
                .addFilter(new JwtVerifyFilter(authenticationManager(), rsaKeyProperties))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

6. controller

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @RequestMapping("/findAll")
    public String findAll(){
        return productService.findAll();
    }
}

7. 启动类

@SpringBootApplication
@MapperScan("com.stone.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class)
public class AuthSourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthSourceApplication.class, args);
    }
}

8. 测试

postman

POST请求:http://localhost:9002/product/findAll

Headers中添加:

Key:Authorization
Value:Bearer [登录测试获取到的token]

此时我们就可以访问对应的资源

三、结尾

以上即为Spring Security-整合Spring Boot(分布式)的全部内容,感谢阅读。

上一篇 下一篇

猜你喜欢

热点阅读