Spring Security安全管理

Spring Security安全管理

目前主流的安全管理框架主要有Spring Security和Shiro。Shiro是一个轻量级框架,配置较为简单。而Spring Security则较为复杂,但功能相对较多。

Spring Boot 中对Spring Security做了一系列自动化配置,使得在Spring Boot中使用Spring Security相当方便。

Spring Security

当引入Spring Security依赖后,所有的接口都将被保护起来,访问接口时需要输入用户名和密码。用户名默认为user,密码在控制台随机生成。这是spring boot 为spring security提供的自动化配置。

当然,登录的用户名可以自己配置,配置的方法主要有两种

  • 在配置文件中配置

在application.properties中添加如下配置,即可设置登录的用户名和密码

spring.security.user.name=admin
spring.security.user.password=123
spring.security.user.roles=admin
  • 使用Java代码配置
    创建Security配置类,继承自WebSecurityConfigurerAdapter类,重写
    configure(AuthenticationManagerBuilder auth)方法,如下:
 @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("admin").password("123").roles("admin")
            .and()
            .withUser("zby").password("123").roles("user");
    }

这里设置了两个用户admin和zby,用户角色分别是admin和user。

HttpSecurity

HttpSecurity是Spring Security Config用于配置http请求安全控制的安全构建器(类似于Spring Security XML配置中的http命名空间配置部分),它的构建目标是一个SecurityFilterChain,实现类使用DefaultSecurityFilterChain。该目标SecurityFilterChain最终会被Spring Security的安全过滤器FilterChainProxy所持有和应用于相应的http请求的安全控制。

spring security类中为我们提供了configure(HttpSecurity http),可以在这个方法中配置拦截规则,实现http请求的安全管理

使用方法如下:

   @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() //开启配置
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("user/**").hasAnyRole("admin","user")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/doLogin")
                .permitAll()
                .and()
                .csrf().disable();
    }

控制器

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
    @GetMapping("/admin/hello")
    public String admin(){
        return "hello admin";
    }
    @GetMapping("/user/hello")
    public String user(){
        return "hello user";
    }
}

这里设置了admin、user角色的访问权限,/admin的接口仅允许角色为admin的用户访问,/user接口角色为user和admin用户都可访问,其他页面登录后即可访问。
登录zby用户后,若要访问/admin/hello接口,浏览将会报错,显示没有权限

This application has no explicit mapping for /error, so you are seeing this as a fallback.

Tue Feb 25 15:11:50 CST 2020
There was an unexpected error (type=Forbidden, status=403).
Forbidden

/hello和/user/hello则可以正常访问。

配置多个HttpSecurity

  @Configuration
    @Order(1)
    public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/admin/**").authorizeRequests().anyRequest().hasRole("admin");
        }
    }

    @Configuration
    public static class OtherSecurity extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginProcessingUrl("/login")
                    .permitAll()
                    .and()
                    .csrf().disable();
        }
    }

表单登录配置

配置表单登录时我们可以在 successHandler方法中,配置登录成功的回调,如果是前后端分离开发的话,登录成功后返回 JSON 即可,同理,failureHandler 方法中配置登录失败的回调,logoutSuccessHandler 中则配置注销成功的回调。

//登录成功的处理器
.successHandler(new AuthenticationSuccessHandler() {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        Map<String, Object> map = new HashMap<>();
        map.put("status",200);
        map.put("msg",authentication.getPrincipal());
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
})

登录成功后返回的json

{
    "msg": {
        "password": null,
        "username": "admin",
        "authorities": [
            {
                "authority": "ROLE_admin"
            }
        ],
        "accountNonExpired": true,
        "accountNonLocked": true,
        "credentialsNonExpired": true,
        "enabled": true
    },
    "status": 200
}

登录失败的处理器

 //登录失败的处理器
.failureHandler(new AuthenticationFailureHandler() {
    @Override
    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        Map<String, Object> map = new HashMap<>();
        map.put("status",401);
        if(e instanceof LockedException){
            map.put("msg","账号被锁定,登录失败");
        }else if(e instanceof BadCredentialsException){
            map.put("msg","用户名或密码错误,登录失败");
        }
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
})

注销登录

.logoutSuccessHandler(new LogoutSuccessHandler() {
    @Override
    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        Map<String, Object> map = new HashMap<>();
        map.put("status",200);
        map.put("msg","注销成功");
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
})

注销成功,返回的json

{
    "msg": "注销成功",
    "status": 200
}

密码加密

在Spring5之后,密码必须加密后才能应用。加密密码则需要配置一个密码的编码器,可以通过PasswordEncoder实现

spring security中提供了BCryptPasswordEncoder工具进行密码加密,如将同一串进行十次加密

  @Test
    void contextLoads() {
        for (int i = 0; i < 10; i++) {
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            System.out.println(encoder.encode("123"));
        }
    }

得到结果如下:


image

同一串字符每次加密产生的结构不同,这就实现了密码的加密。

方法安全

Spring Security框架支持通过在方法上加注解来确保方法的安全。

方法安全在Spring Security中默认是没有开启的,在Spring Security配置类上加@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true),开启方法安全的相关注解

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MultiHttpSecurityConfig{
    ......
}

创建Service类

@Service
public class MethodService {
   @PreAuthorize("hasRole('admin')")
    public String admin(){
        return "hello admin";
    }
    @Secured("ROLE_user")
    public String user(){
        return "hello user";
    }
    @PreAuthorize("hasAnyRole('admin','author')")
    public String hello(){
        return "hello world";
    }
}

为三个方法分别赋予相应的角色。

在Controller中调用三个方法

@Autowired
    MethodService methodService;
    @GetMapping("/hello1")
    public String hello1(){
        return methodService.admin();
    }
    @GetMapping("/hello2")
    public String hello2(){
        return methodService.user();
    }
    @GetMapping("/hello3")
    public String hello3(){
        return methodService.hello();
    }

这时每个接口都可以被访问,但只有相应的角色才能调用接口中的方法。

基于数据库的认证

创建项目后并配置数据库信息

spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=admin
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

创建User和Role的实体类

@Data
public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean locked;
    private List<Role> roles;
    //返回用户所以角色
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
    //账户是否未过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    //账户是否未锁定
    @Override
    public boolean isAccountNonLocked() {
        return locked;
    }
    //密码是否未过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    //是否可用
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

这里定义用户时需要实现UserDetails接口。

@Data
public class Role {
    private Integer id;
    private String name;
    private String nameZh;
}

Service:

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if(user == null){
            throw new UsernameNotFoundException("用户名不存在");
        }
        user.setRoles(userMapper.getUserRolesById(user.getId()));
        return user;
    }
}

配置SpringSecurity

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserService userService;

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

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

角色继承的配置

@Bean
RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user";
    roleHierarchy.setHierarchy(hierarchy);
    return roleHierarchy;
}

配置完成后,接下来指定角色和资源的对应关系即可,如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers("/admin/**")
            .hasRole("admin")
            .antMatchers("/db/**")
            .hasRole("dba")
            .antMatchers("/user/**")
            .hasRole("user")
            .and()
            .formLogin()
            .loginProcessingUrl("/doLogin")
            .permitAll()
            .and()
            .csrf().disable();
}

这个表示 /db/** 格式的路径需要具备 dba 角色才能访问, /admin/** 格式的路径则需要具备 admin 角色才能访问, /user/** 格式的路径,则需要具备 user 角色才能访问,此时提供相关接口,会发现,dba 除了访问 /db/** ,也能访问 /admin/** 和 /user/** ,admin 角色除了访问 /admin/** ,也能访问 /user/** ,user 角色则只能访问 /user/** 。

动态权限配置

动态权限配置就是要将权限也存入数据库中,通过数据库中数据之间的关系来确定权限。
数据库权限如下图所示
[图片上传失败...(image-a9aca8-1582887592116)]
通过user确定role,在通过role定位到相应的menu

要实现动态权限配置,首先要配置过滤器,创建一个filter类,实现FilterInvocationSecurityMetadataSource接口,并实现接口中的方法

@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource {
    //路径匹配符
    AntPathMatcher pathMatcher = new AntPathMatcher();
    @Autowired
    MenuService menuService;
    //根据请求地址,分析请求需要的角色
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        List<Menu> allMenus = menuService.getAllMenus();
        for (Menu menu : allMenus) {
            if(pathMatcher.match(menu.getPattern(),requestUrl)){
                List<Role> roles = menu.getRoles();
                String[] rolesStr = new String[roles.size()];
                for (int i = 0; i < roles.size(); i++) {
                    rolesStr[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(rolesStr);
            }
        }
        return SecurityConfig.createList("ROLE_login");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

另外还需要配置一个类用于查询是否具备请求需要的角色,若不存在则该请求是一个非法请求,该类要实现AccessDecisionManager接口。该接口提供了三个方法decide方法和两个supports方法,两个supports方法默认返回值为true。decide方法中有三个参数

void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection)
  • authentication:保存当前登录用户信息,代表用户拥有的角色
  • o:是一个FilterInvocation对象,用于获取当前请求对象,代表需要的角色
  • collection:是MyFilter类中Collection<ConfigAttribute> getAttributes(Object o)的返回值
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute attribute : collection) {
            if("ROLE_login".equals(attribute.getAttribute())){
                //判断是否登录,若是匿名用户则表示没有登录,抛出异常
                if(authentication instanceof AnonymousAuthenticationToken){
                    throw new AccessDeniedException("非法请求!");
                }else break;
            }
            //获取当前用户具备的角色
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if(authority.getAuthority().equals(attribute.getAttribute())){
                    break;
                }
            }
        }
        throw new AccessDeniedException("非法请求!");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

两个类写完之后,在security配置类中引入,并在HttpSecurity方法中做如下配置:

@Autowired
MyFilter myFilter;
@Autowired
MyAccessDecisionManager myAccessDecisionManager;

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                    o.setAccessDecisionManager(myAccessDecisionManager);
                    o.setSecurityMetadataSource(myFilter);
                    return o;
                }
            })
            .and()
            .formLogin()
            .permitAll()
            .and()
            .csrf().disable();
    }

编写接口测试

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
    @GetMapping("/db/hello")
    public String db(){
        return "hello db";
    }

    @GetMapping("/admin/hello")
    public String admin(){
        return "hello admin";
    }

    @GetMapping("/user/hello")
    public String user(){
        return "hello user";
    }
}

OAuth2协议

OAuth(Open Authorization,开放授权)是为用户资源的授权定义了一个安全、开放及简单的标准,第三方无需知道用户的账号及密码,就可获取到用户的授权信息

  • 应用场景

第三方应用授权登录:在APP或者网页接入一些第三方应用时,时常会需要用户登录另一个合作平台,比如QQ,微博,微信的授权登录,第三方应用通过oauth2方式获取用户信息

具体的实现流程图如下:


  • Spring Security结合OAuth2
    Spring Boot下的OAuth2是在spring security的基础上完成的。
    添加OAuth2的依赖:
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.6.RELEASE</version>
</dependency>

将OAuth中的Token令牌放在Redis中,因此需要再添加Redis依赖:

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

配置Redis

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0

在OAuth2中需要配置两个服务器,一个授权服务器和一个资源服务器
1.配置授权服务器

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("password")
                .authorizedGrantTypes("password","refresh_token")//配置授权模式
                .accessTokenValiditySeconds(1800)//Token过期时间
                .resourceIds("rid")
                .scopes("all")
                .secret("$2a$10$9zMfB82E5BnYvnKriQUdaudC39H5JEu.HN80ywI2EQY/2.MuOj.i.");
    }

    //配置Token存取
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    //支持clientId和client security做登录认证
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
    }

2.配置资源服务器

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("rid").stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated();
    }
}

根据OAuth2协议,先从授权服务器中获取Token,再到资源服务器上获取资源,判断给出的Token令牌是否有权限访问资源。

最后配置Security

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
    @Override
    @Bean
    protected UserDetailsService userDetailsService() {
        return super.userDetailsService();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("zby")
                .password("$2a$10$9zMfB82E5BnYvnKriQUdaudC39H5JEu.HN80ywI2EQY/2.MuOj.i.")
                .roles("admin")
                .and()
                .withUser("user")
                .password("$2a$10$9zMfB82E5BnYvnKriQUdaudC39H5JEu.HN80ywI2EQY/2.MuOj.i.")
                .roles("user");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/oauth/**")
                .authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .and().csrf().disable();
    }
}
  • 测试
    在PostMan中,向测试接口发送请求,可得到token


    image

Redis中存储的Token信息


image

用Token去访问相应资源


image

token过期时可利用refresh_token参数,通过post请求获取新的token

发送请求:http://localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=1235097a-d9fd-4342-9c05-a0c2b535b166&client_id=password&client_secret=123

得到新的token

{
    "access_token": "37a62e16-0774-4fc4-b043-824343b3709b",
    "token_type": "bearer",
    "refresh_token": "1235097a-d9fd-4342-9c05-a0c2b535b166",
    "expires_in": 1799,
    "scope": "all"
}

Spring Security使用Json登录

keyValue形式的登录主要通过过滤器UsernamePasswordAuthenticationFilter来实现。所以,要实现Json登录需要重新一个过滤器。

创建过滤器MyFilter类,继承UsernamePasswordAuthenticationFilter并重写attemptAuthentication方法。

public class MyFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //先判断发来的是否是Post请求
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        //解析Json
        if(request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
            //若if条件成立,说明用户以JSON形式传递参数
            String username = null;
            String password = null;

            try {
                Map<String,String> map = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                username = map.get("username");
                password = map.get("password");
            } catch (IOException e) {
                e.printStackTrace();
            }


            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
        //否则调用父类的方法登录
        return super.attemptAuthentication(request, response);
    }
}

之后,在Security配置类中进行配置,使MyFilter中的方法生效

整合JWT

JWT,全称是Json Web Token,是一种JSON风格的轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权。特别适用于分布式站点的单点登录(SSO)场景。

jwt数据格式

jwt数据格式一般包括三部分:

1.头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。对头部进行Base64Url编码(可解码),得到第一部分数据。

2.载荷(Payload)

就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:

  • iss (issuer):表示签发人
  • exp (expiration time):表示token过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号
    这部分也会采用Base64Url编码,得到第二部分数据。

3.签名(Signature)

是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥secret(密钥保存在服务端,不能泄露给客户端),通过Header中配置的加密算法生成。用于验证整个数据完整和可靠性。

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT交互流程

1.应用程序或客户端向授权服务器请求授权
2.获取到授权后,授权服务器会向应用程序返回访问令牌
3、应用程序使用访问令牌来访问受保护资源(如API)

因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,这样就完全符合了RESTful的无状态规范。

在Spring Security中整合JWT

首先创建一个Spring Boot项目,创建时添加Spring Security依赖,创建完成后,添加 jjwt 依赖,pom.xml文件如下:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

然后在项目中创建一个简单的 User 对象实现 UserDetails 接口。

再创建一个HelloController,内容如下:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello jwt !";
    }
    @GetMapping("/admin")
    public String admin() {
        return "hello admin !";
    }
}

HelloController有两个接口,设计是 /hello 接口可以被具有 user 角色的用户访问,而 /admin 接口则可以被具有 admin 角色的用户访问。

接下来提供两个和 JWT 相关的过滤器配置:

一个是用户登录的过滤器,在用户的登录的过滤器中校验用户是否登录成功,如果登录成功,则生成一个token返回给客户端,登录失败则给前端一个登录失败的提示。用户登录的过滤器 JwtLoginFilter 继承自 AbstractAuthenticationProcessingFilter,并实现其中的三个默认方法。
在attemptAuthentication方法中,从登录参数中提取出用户名密码,然后调用AuthenticationManager.authenticate()方法去进行自动校验。

如果校验成功,就会来到successfulAuthentication回调中,在successfulAuthentication方法中,将用户角色遍历然后用一个“,”连接起来,然后再利用Jwts去生成token,按照代码的顺序,生成过程一共配置了四个参数,分别是用户角色、主题、过期时间以及加密算法和密钥,然后将生成的token写出到客户端。

如果校验失败就会来到unsuccessfulAuthentication方法中,在这个方法中返回一个错误提示给客户端即可。

public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
    public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException {
        User user = new ObjectMapper().readValue(req.getInputStream(),User.class);
        return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword()));
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();//获取登录用户角色
        StringBuffer sb = new StringBuffer();
        for (GrantedAuthority authority : authorities) {
            sb.append(authority.getAuthority()).append(",");
        }
        String jwt = Jwts.builder()
                .claim("authorities", sb)
                .setSubject(authResult.getName())
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
                .signWith(SignatureAlgorithm.HS512, "zby@123")
                .compact();//生成JWT的Token
        Map<String,String> map = new HashMap<>();
        map.put("token",jwt);
        map.put("msg","登录成功");
        resp.setContentType("application/json:charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException {
        Map<String,String> map = new HashMap<>();
        map.put("msg","登录失败");
        resp.setContentType("application/json:charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
}

第二个过滤器则是当其他请求发送来,校验token的过滤器,如果校验成功,就让请求继续执行。首先从请求头中提取出authorization字段,这个字段对应的value就是用户的token。将提取出来的token字符串转换为一个Claims对象,再从Claims对象中提取出当前用户名和用户角色,创建一个UsernamePasswordAuthenticationToken放到当前的Context中,然后执行过滤链使请求继续执行下去。

public class JwtFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        String jwtToken = req.getHeader("authorization");
        Jws<Claims> jws = Jwts.parser().setSigningKey("zby@123")
                .parseClaimsJws(jwtToken.replace("Bearer", ""));
        Claims claims = jws.getBody();
        String username = claims.getSubject();
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(((String) claims.get("authorities")));
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
        SecurityContextHolder.getContext().setAuthentication(token);
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

两个过滤器配置好后,在Security配置类中添加两个过滤器

 @Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/hello")
        .hasRole("user")
        .antMatchers("/admin")
        .hasRole("admin")
        .antMatchers(HttpMethod.POST,"/login")
        .permitAll()
        .anyRequest().authenticated()
        .and()
        .addFilterBefore(new JwtLoginFilter("/login",authenticationManager()), UsernamePasswordAuthenticationFilter.class)
         .addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class)
         .csrf().disable();
    }

配置路径规则时, /hello 接口必须要具备 user 角色才能访问, /admin 接口必须要具备 admin 角色才能访问,POST 请求并且是 /login 接口则可以直接通过,其他接口必须认证后才能访问。

登陆成功,返回一个Json

{
    "msg": "登录成功",
    "token": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfYWRtaW4sIiwic3ViIjoiYWRtaW4iLCJleHAiOjE1ODI3OTM0NjR9.4cTTZpjl1j2YxldmTHWbK6oN0htJn-kW9V2p6Nj7jc26znegUmtrXohy0dgH4uDH053UL4-IICSo_ETzJJtmeQ"
}

登录成功后返回一个token,请求资源时需要提供token才能正常访问


image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容