SpringBoot2.0实战 | 第二十七章:整合SpringSecurity之前后端分离使用Token实现登录鉴权

通过前一篇文章,我们已经实现了前后端分离模式下,使用JSON数据进行前后端交互

主要涉及到

  • 实现 AbstractAuthenticationProcessingFilter 接口,接收JSON格式登录表单数据,执行登录校验
  • 实现 AuthenticationSuccessHandler 接口,登录成功,返回JSON格式信息
  • 实现 AuthenticationFailureHandler 接口,登录失败,返回JSON格式错误信息
  • 实现 AccessDeniedHandler 接口,登录成功,但无资源访问权限时,返回JSON格式错误信息
  • 实现 AuthenticationEntryPoint 接口,未登录访问资源时,返回JSON格式错误信息

要实现前后端分离,还有一个重要的环节就是存储用户登录状态,
在前一篇文章中,虽然我们实现的JSON格式交互,但是依然使用 session 存储用户登录状态,
但是在实际项目中,客户端不再是单纯的网页,还可以是手机,平板,公众号,小程序等,
不是每一个客户端都能够支持 session+cookie 的模式,怎么样可以使用一套代码,实现多个客户端登录、鉴权。

使用 token 代替 session 的流程:
用户登录成功,服务端向客户端分发一个 token,
客户端根据自已的情况自行存储,并且在每一次请求中附带上该 token,
服务端接收到请求,对该 token 进行校验,判断请求用户登录状态,获取权限信息,实现权限校验。

目标

整合 SpringSecurity 实现使用 token 进行鉴权。

思路

分成两个部分,第一部分是登录,客户端向服务端发起登录请求时,
服务端需要生成token并存储起来,然后将token分发给客户端,客户端需要自行存储该token。

流程图:

客户端 服务端 数据库 提交表单 生成token 存储token 分发token 客户端 服务端 数据库




在第二部分是登录成功后,访问资源时,客户端需要将登录时收到的token,附加在请求中,发送给服务端,
服务端需要判断该token的有效性,并通过该token,可以获取到当前用户信息。

流程图:

客户端 服务端 数据库 访问资源 获取token 通过token获取用户信息 客户端 服务端 数据库



本文使用数据库存储token,实际项目中会使用 redis 之类的更高效的存储


准备工作

创建用户表 user、角色表 role、用户角色关系表 user_roletoken

CREATE TABLE `role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `rolename` varchar(32) NOT NULL COMMENT '角色名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COMMENT='角色';

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) NOT NULL COMMENT '用户名',
  `password` varchar(128) NOT NULL COMMENT '密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COMMENT='用户';

CREATE TABLE `user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL,
  `role_id` bigint(20) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`,`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关系表';

CREATE TABLE `token` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `token` varchar(128) NOT NULL COMMENT 'token',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='token';

操作步骤

添加依赖

引入 Spring Boot Starter 父工程

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.5.RELEASE</version>
</parent>

添加 springSecuritymybatisPlus 的依赖,添加后的整体依赖如下

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

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

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

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

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.2.0</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>
配置

配置一下数据源

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8&useSSL=false
    username: app
    password: 123456
编码
实体类

角色实体类 Role,实现权限接口 GrantedAuthority

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("role")
public class Role implements GrantedAuthority {

    @TableId(type = IdType.AUTO)
    private Long id;
    private String rolename;

    @Override
    public String getAuthority() {
        return this.rolename;
    }
}

用户实体类 user,实现权限接口 UserDetails,主要方法是 getAuthorities,用于获取用户的角色列表

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
public class User implements UserDetails {

    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private String password;
    @TableField(exist = false)
    private List<Role> roleList;
    @TableField(exist = false)
    private String token;

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

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

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

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

    @Override
    public boolean isEnabled() {
        return true;
    }

}

用户角色关系实体

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user_role")
public class UserRole {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private Long roleId;
}

Token实体

@Data
@TableName("token")
public class Token {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String token;
    private Long userId;
}
Repository 层

分别为四个实体类添加 Mapper

@Mapper
public interface RoleRepository extends BaseMapper<Role> {
}
@Mapper
public interface UserRepository extends BaseMapper<User> {
}
@Mapper
public interface UserRoleRepository extends BaseMapper<UserRole> {
}
@Mapper
public interface TokenRepository extends BaseMapper<Token> {
}
权限配置
实现 UserDetailsService 接口

UserDetailsService 是 SpringSecurity 提供的登陆时用于根据用户名获取用户信息的接口

@AllArgsConstructor
@Service
public class UserService implements UserDetailsService {

    private UserRepository userRepository;
    private RoleRepository roleRepository;
    private UserRoleRepository userRoleRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (username == null || username.isEmpty()) {
            throw new UsernameNotFoundException("用户名不能为空");
        }
        User user = userRepository.selectOne(
                new QueryWrapper<User>().lambda().eq(User::getUsername, username));
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        List<UserRole> userRoles = userRoleRepository.selectList(
                new QueryWrapper<UserRole>().lambda().eq(UserRole::getUserId, user.getId()));
        if (userRoles != null && !userRoles.isEmpty()) {
            List<Long> roleIds = userRoles.stream()
                                        .map(UserRole::getRoleId)
                                        .collect(Collectors.toList());
            List<Role> roles = roleRepository.selectList(
                    new QueryWrapper<Role>().lambda().in(Role::getId, roleIds));
            user.setRoleList(roles);
        }
        return user;
    }

}
自定义登录参数格式
@Data
public class LoginDto {

    private String mobile;
    private String password;
    private String dycode;

}
自定义登录鉴权过滤器

继承 SpringSecurity 提供的 AbstractAuthenticationProcessingFilter 类,实现 attemptAuthentication 方法,用于登录校验。
本例中,模拟前端使用 json 格式传递参数,所以通过 objectMapper.readValue 的方式从流中获取入参,之后借用了用户名密码登录的校验,
如果鉴权成功,则生成 token 存库并将 token 返回给前端。

@Data
public class JsonAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private TokenRepository tokenRepository;

    public JsonAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, 
                                                HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
        try {
            LoginDto loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginDto.class);
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                    loginUser.getMobile(), loginUser.getPassword());
            Authentication authenticate = getAuthenticationManager().authenticate(token);
            if (authenticate.isAuthenticated()) {
                User user = (User) authenticate.getPrincipal();
                Token token = new Token();
                token.setToken(UUID.randomUUID().toString());
                token.setUserId(user.getId());
                tokenRepository.insert(token);
                user.setToken(token.getToken());
            }
            return authenticate;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}
自定义登陆成功后处理

实现 SpringSecurity 提供的 AuthenticationSuccessHandler 接口,使用 JSON 格式返回

@AllArgsConstructor
public class JsonLoginSuccessHandler implements AuthenticationSuccessHandler {

    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }

}
自定义登陆失败后处理

实现 SpringSecurity 提供的 AuthenticationFailureHandler 接口,使用 JSON 格式返回

public class JsonLoginFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, 
                                        AuthenticationException exception) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write("{\"message\":\"" + exception.getMessage() + "\"}");
    }

}
自定义权限校验失败后处理

登陆成功之后,访问接口之前 SpringSecurity 会进行鉴权,如果没有访问权限,需要对返回进行处理。
实现 SpringSecurity 提供的 AccessDeniedHandler 接口,使用 JSON 格式返回

public class JsonAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException exception) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write("{\"message\":\"" + exception.getMessage() + "\"}");
    }

}
自定义未登录后处理

实现 SpringSecurity 提供的 AuthenticationEntryPoint 接口,使用 JSON 格式返回

public class JsonAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException exception) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write("{\"message\":\"" + exception.getMessage() + "\"}");
    }

}
自定义 Token 验证过滤器

客户端登录成功时,后台会把生成的 token 返回给前端,之后客户端每次请求后台接口将会把这个 token 附在 header 头中传递给后台,
后台会验证这个 token 是否有效,如果有效就把用户信息加载至 SpringSecurity 中,如果无效则会跳转至上一步提供 AuthenticationEntryPoint 进行处理。

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private TokenRepository tokenRepository;
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private RoleRepository roleRepository;
    @Autowired
    private UserRoleRepository userRoleRepository;

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, FilterChain filterChain) 
                        throws ServletException, IOException {
        String tokenStr = request.getHeader("token");
        if (tokenStr != null && !tokenStr.isEmpty()) {
            Token tokenDb = tokenRepository.selectOne(
                    new QueryWrapper<Token>().lambda().eq(Token::getToken, tokenStr));
            if (tokenDb != null && tokenDb.getUserId() != null) {
                User user = userRepository.selectById(tokenDb.getUserId());
                if (user == null) {
                    throw new UsernameNotFoundException("token已失效");
                }
                List<UserRole> userRoles = userRoleRepository.selectList(
                        new QueryWrapper<UserRole>().lambda().eq(UserRole::getUserId, user.getId()));
                if (userRoles != null && !userRoles.isEmpty()) {
                    List<Long> roleIds = userRoles.stream()
                                            .map(UserRole::getRoleId)
                                            .collect(Collectors.toList());
                    List<Role> roles = roleRepository.selectList(
                            new QueryWrapper<Role>().lambda().in(Role::getId, roleIds));
                    user.setRoleList(roles);
                }
                user.setToken(tokenStr);
                UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                logger.info(String.format("Authenticated user %s, setting security context", user.getUsername()));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(request, response);
    }

}
注册

在 configure 方法中将自定义的 jsonAuthenticationFilter 及 tokenAuthenticationFilter 注册进 SpringSecurity 的过滤器链中,
并禁用 session。

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
            .and().csrf().disable()// 禁用 csrf
                // 禁用 session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                .exceptionHandling()
                    .authenticationEntryPoint(new JsonAuthenticationEntryPoint())
                    .accessDeniedHandler(new JsonAccessDeniedHandler())
            .and()
                .addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilterAfter(jsonAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter();
    }

    @Bean
    public JsonAuthenticationFilter jsonAuthenticationFilter() throws Exception {
        JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManager());
        filter.setAuthenticationSuccessHandler(jsonLoginSuccessHandler());
        filter.setAuthenticationFailureHandler(new JsonLoginFailureHandler());
        return filter;
    }

    @Bean
    public JsonLoginSuccessHandler jsonLoginSuccessHandler() {
        return new JsonLoginSuccessHandler(objectMapper);
    }
}
启动类
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

验证结果

初始化数据

执行测试用例进行初始化数据

@Slf4j
@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = Application.class)
public class SecurityTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserRoleRepository userRoleRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Test
    public void initData() {
        List<User> userList = new ArrayList<>();
        userList.add(new User(1L, "admin", new BCryptPasswordEncoder().encode("123456"), null));
        userList.add(new User(2L, "user", new BCryptPasswordEncoder().encode("123456"), null));

        List<Role> roleList = new ArrayList<>();
        roleList.add(new Role(1L, "ROLE_ADMIN"));
        roleList.add(new Role(2L, "ROLE_USER"));

        List<UserRole> urList = new ArrayList<>();
        urList.add(new UserRole(1L, 1L, 1L));
        urList.add(new UserRole(2L, 1L, 2L));
        urList.add(new UserRole(3L, 2L, 2L));

        userList.forEach(userRepository::insert);
        roleList.forEach(roleRepository::insert);
        urList.forEach(userRoleRepository::insert);
    }

}

源码地址

本章源码 : https://gitee.com/gongm_24/spring-boot-tutorial.git

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
好的,你想了解如何在Spring Boot整合Spring Security和OAuth 2.0实现token认证。下面是一个基本的步骤: 1. 添加依赖:在你的`pom.xml`文件中添加Spring Security和OAuth 2.0的依赖。 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> ``` 2. 配置Spring Security:创建一个继承自`WebSecurityConfigurerAdapter`的配置类,并覆盖`configure`方法,配置Spring Security。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/oauth2/**", "/login/**", "/logout/**") .permitAll() .anyRequest() .authenticated() .and() .oauth2Login() .loginPage("/login") .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/") .deleteCookies("JSESSIONID"); } } ``` 这个配置类指定了哪些URL需要进行认证,`antMatchers`方法指定了不需要认证的URL。 3. 配置OAuth 2.0客户端:创建一个继承自`WebSecurityConfigurerAdapter`的配置类,并使用`@EnableOAuth2Client`注解开启OAuth 2.0客户端。 ```java @Configuration @EnableOAuth2Client public class OAuth2ClientConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 配置HTTP Security } @Bean public OAuth2AuthorizedClientService authorizedClientService( OAuth2ClientRegistrationRepository clientRegistrationRepository) { return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); } } ``` 这个配置类可以用来配置OAuth 2.0的客户端,你可以在`configure`方法中添加一些额外的配置。 4. 配置OAuth 2.0客户端注册:在`application.properties`文件中配置OAuth 2.0的客户端注册信息。 ```properties spring.security.oauth2.client.registration.my-client-id.client-id=your-client-id spring.security.oauth2.client.registration.my-client-id.client-secret=your-client-secret spring.security.oauth2.client.registration.my-client-id.scope=your-scopes spring.security.oauth2.client.registration.my-client-id.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.my-client-id.redirect-uri=your-redirect-uri ``` 这个配置文件中的`my-client-id`是你自己指定的客户端ID,你需要将其替换为你自己的信息。 这些步骤是实现Spring Boot整合Spring Security和OAuth 2.0实现token认证的基本步骤。你可以根据自己的需求进行进一步的配置和扩展。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值