Дизайн системы авторизации на основе Spring Security и JWT

Spring Boot
Дизайн системы авторизации на основе Spring Security и JWT

написать впереди

  • О весенней безопасностиМодуль аутентификации и полномочий веб-системы также можно рассматривать как инфраструктуру системы, и почти любая служба Интернета будет иметь требования в этом отношении. В области Java EE зрелые решения для обеспечения безопасности обычно включают Apache Shiro и Spring Security. Простота и легкость использования Apache Shiro также являются большим преимуществом, но его функции по-прежнему намного менее эффективны, чем Spring Security. Spring Security может обеспечить декларативный контроль доступа к безопасности для приложений Spring, предоставляя серию bean-компонентов, которые можно настроить в контексте приложения Spring, и используя Spring IoC и AOP, а также другие функциональные возможности для обеспечения декларативного контроля доступа к безопасности для функций прикладных систем, уменьшая много дублирования работы.

  • О JWTJSON Web Token (JWT) — открытый стандарт на основе JSON (RFC 7519) для передачи информации между сетевыми приложениями.Он используется в качестве объекта JSON для безопасной передачи информации между различными системами.Основные сценарии использования, как правило, используют для передачи аутентифицированного информация об удостоверении пользователя между поставщиком удостоверений и поставщиком услуг.Для научно-популярной информации о JWT вы можете посмотреть «Вводный курс JSON Web Token» г-на Руана Ифэна.

В этой статье Spring Security и JWT объединяются для создания простой системы разрешений.

Экспериментальная среда этой статьи выглядит следующим образом:

  • Весенняя загрузочная версия:2.0.6.RELEASE
  • ИДЕ:IntelliJ IDEA 2018.2.4

Кроме того, экспериментальный код этой статьи размещен в конце текста и его нужно подобрать самостоятельно.

МожетНажмитеилисканированиеследующеебудь остороженподписаться на официальный аккаунт автораCodeSheep, получить большеПрагматичный, понятный, воспроизводимыйИсходный текст ↓↓↓

CodeSheep · 程序羊


Дизайн пользователей и ролей

Для того, чтобы упростить рассмотрение эксперимента в этой статье, мы собираемся сделать следующий план:

  • Создайте минимальную таблицу ролейrole,включают角色IDи角色名称

角色表

  • Создайте минимальную пользовательскую таблицуuser,включают用户ID,用户名,密码

用户表

  • Создайте таблицу связей «один ко многим» между пользователями и ролямиuser_rolesПользователь может иметь несколько ролей
    用户-角色对应表

Создайте веб-проект, поддерживаемый Spring Security и JWT.

  • pom.xmlВвести зависимости, необходимые для Spring Security и JWT.
<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>
  • Добавьте необходимую конфигурацию, такую ​​как база данных и JPA, в файл конфигурации проекта.
server.port=9991

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://121.196.XXX.XXX:3306/spring_security_jwt?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=XXXXXX

logging.level.org.springframework.security=info

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jackson.serialization.indent_output=true
  • Создание пользователей, ролевых сущностей

Объект пользователя Пользователь:

/**
 * @ www.codesheep.cn
 * 20190312
 */
@Entity
public class User implements UserDetails {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    private String password;

    @ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
    private List<Role> roles;

    ...

    // 下面为实现UserDetails而需要的重写方法!
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add( new SimpleGrantedAuthority( role.getName() ) );
        }
        return authorities;
    }
    
    ...
}

Созданный здесь класс User наследует интерфейс UserDetails Spring Security, таким образом становясь пользователем, совместимым с Security, то есть, наследуя UserDetails, можно реализовать соответствующие функции безопасности в Security.

Ролевая сущность Роль:

/**
 * @ www.codesheep.cn
 * 20190312
 */
@Entity
public class Role {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
  
    ... // 省略 getter和 setter
}
  • Создать класс инструмента JWT

Он в основном используется для выполнения различных операций с токеном JWT, таких как создание токена, проверка токена, обновление токена и т. д.

/**
 * @ www.codesheep.cn
 * 20190312
 */
@Component
public class JwtTokenUtil implements Serializable {

    private static final long serialVersionUID = -5625635588908941275L;

    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";

    public String generateToken(UserDetails userDetails) {
        ...
    }

    String generateToken(Map<String, Object> claims) {
        ...
    }

    public String refreshToken(String token) {
        ...
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        ...
    }

    ... // 省略部分工具函数
}
  • Создайте Token filter для обработки Token для каждого внешнего запроса к интерфейсу
/**
 * @ www.codesheep.cn
 * 20190312
 */
@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal ( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        String authHeader = request.getHeader( Const.HEADER_STRING );
        if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) {
            final String authToken = authHeader.substring( Const.TOKEN_PREFIX.length() );
            String username = jwtTokenUtil.getUsernameFromToken(authToken);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                	if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
                                request));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
            }
        }
        chain.doFilter(request, response);
    }
}
  • Служебная деловая переписка

В основном он включает в себя два основных направления входа и регистрации пользователей.

public interface AuthService {
    User register( User userToAdd );
    String login( String username, String password );
}
/**
 * @ www.codesheep.cn
 * 20190312
 */
@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserRepository userRepository;

    // 登录
    @Override
    public String login( String username, String password ) {
        UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password );
        final Authentication authentication = authenticationManager.authenticate(upToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        final UserDetails userDetails = userDetailsService.loadUserByUsername( username );
        final String token = jwtTokenUtil.generateToken(userDetails);
        return token;
    }

    // 注册
    @Override
    public User register( User userToAdd ) {
        final String username = userToAdd.getUsername();
        if( userRepository.findByUsername(username)!=null ) {
            return null;
        }
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        final String rawPassword = userToAdd.getPassword();
        userToAdd.setPassword( encoder.encode(rawPassword) );
        return userRepository.save(userToAdd);
    }
}
  • Написание класса конфигурации Spring Security (очень важно)

Это очень всеобъемлющий класс конфигурации, в основном переопределяющийWebSecurityConfigurerAdapterчастьconfigureКонфигурация для реализации пользовательских частей.

/**
 * @ www.codesheep.cn
 * 20190312
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Bean
    public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
        return new JwtTokenFilter();
    }

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

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

    @Override
    protected void configure( HttpSecurity httpSecurity ) throws Exception {
        httpSecurity.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS请求全部放行
                .antMatchers(HttpMethod.POST, "/authentication/**").permitAll()  //登录和注册的接口放行,其他接口全部接受验证
                .antMatchers(HttpMethod.POST).authenticated()
                .antMatchers(HttpMethod.PUT).authenticated()
                .antMatchers(HttpMethod.DELETE).authenticated()
                .antMatchers(HttpMethod.GET).authenticated();

        // 使用前文自定义的 Token过滤器
        httpSecurity
                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

        httpSecurity.headers().cacheControl();
    }
}
  • Написать тестовый контроллер

Логин и зарегистрированный контроллер:

/**
 * @ www.codesheep.cn
 * 20190312
 */
@RestController
public class JwtAuthController {
    @Autowired
    private AuthService authService;

    // 登录
    @RequestMapping(value = "/authentication/login", method = RequestMethod.POST)
    public String createToken( String username,String password ) throws AuthenticationException {
        return authService.login( username, password ); // 登录成功会返回JWT Token给用户
    }

    // 注册
    @RequestMapping(value = "/authentication/register", method = RequestMethod.POST)
    public User register( @RequestBody User addedUser ) throws AuthenticationException {
        return authService.register(addedUser);
    }
}

Затем напишите контроллер для проверки разрешений:

/**
 * @ www.codesheep.cn
 * 20190312
 */
@RestController
public class TestController {

    // 测试普通权限
    @PreAuthorize("hasAuthority('ROLE_NORMAL')")
    @RequestMapping( value="/normal/test", method = RequestMethod.GET )
    public String test1() {
        return "ROLE_NORMAL /normal/test接口调用成功!";
    }

    // 测试管理员权限
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    @RequestMapping( value = "/admin/test", method = RequestMethod.GET )
    public String test2() {
        return "ROLE_ADMIN /admin/test接口调用成功!";
    }
}

Вот два тестовых интерфейса для тестирования проблем, связанных с разрешениями, где интерфейс/normal/testТребуется, чтобы у пользователя была обычная роль (ROLE_NORMAL) можно получить, в то время как интерфейс/admin/testтребует, чтобы пользователь имел роль администратора (ROLE_ADMIN) можно получить доступ.

Далее запускаем проект, экспериментируем и тестируем, чтобы увидеть эффект


Экспериментальная проверка

  • В начале статьи мы находимся в пользовательской таблицеuserвставил имя пользователяcodesheepзаписей и в таблице ролей пользователейuser_rolesпользователямcodesheepНормальная роль назначена(ROLE_NORMAL) и роль администратора (ROLE_ADMIN)

  • Затем войдите в систему пользователя и получите токен JWT, выданный пользователю в фоновом режиме.

用户登录并获得JWT Token

  • Затем войдите в интерфейс проверки разрешений.

Прямой доступ без токена требует общих ролей (ROLE_NORMAL)Интерфейс/normal/testОн прямо подскажет, что доступ недоступен:

不带token访问是不通的

Для доступа с токеном требуются общие роли (ROLE_NORMAL)Интерфейс/normal/testВызов будет успешным:

带token访问OK

По той же причине текущий пользователь имеет роль администратора, поэтому для доступа требуется роль администратора (ROLE_ADMIN)Интерфейс/admin/testтакже удается:

访问需要管理员角色的接口OK

Далее мы получаем пользователя из таблицы ролей пользователейcodesheepудалите права администратора, а затем получите доступ к интерфейсу/admin/test, вы обнаружите, что доступ запрещен из-за отсутствия разрешений:

由于权限不够而被拒绝

После серии экспериментов он также оправдал наши ожидания!


напиши в конце

В этой статье довольно много всего, и, наконец, мы также поместили экспериментальный исходный код этой статьи вна Гитхабе, вы можете взять его сами, если вам это нужно:Адрес загрузки исходного кода

Из-за ограниченных возможностей, если есть ошибки или неуместность, пожалуйста, критикуйте и исправьте их, учитесь и обменивайтесь мнениями вместе!