JWT和Spring Security集成

 

通常情况下,把API直接暴露出去是风险很大的,

我们一般需要对API划分出一定的权限级别,然后做一个用户的鉴权,依据鉴权结果给予用户对应的API

(一)JWT是什么,为什么要使用它?

互联网服务离不开用户认证。一般流程是下面这样。

1、用户向服务器发送用户名和密码。

2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。

3、服务器向用户返回一个 session_id,写入用户的 Cookie。

4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。

5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。

举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?

一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
(引自:阮一峰的网络日志 JSON Web Token 入门教程

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息

JWT的结构

JWT包含了使用.分隔的三部分:

  • Header 头部

  • Payload 负载

  • Signature 签名

JWT的工作流程

下面是一个JWT的工作流程图。模拟一下实际的流程是这样的(假设受保护的API在/protected中)

1.用户导航到登录页,输入用户名、密码,进行登录
2.服务器验证登录鉴权,如果用户合法,根据用户的信息和服务器的规则生成JWT Token
3.服务器将该token以json形式返回(不一定要json形式,这里说的是一种常见的做法)
4.用户得到token,存在localStorage、cookie或其它数据存储形式中。
5.以后用户请求/protected中的API时,在请求的header中加入 Authorization: Bearer xxxx(token)。此处注意token之前有一个7字符长度的 Bearer
6.服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。
7.用户取得结果

(二)SpringSecurity

Spring Security 是为基于Spring的应用程序提供声明式安全保护的安全性框架。

一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)

两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,

也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。

系统通过校验用户名和密码来完成认证过程。

用户授权指的是验证某个用户是否有权限执行某个操作。

在一个系统中,不同用户所具有的权限是不同的。

比如对一个文件来说,有的用户只能进行读取,

而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,

而每个角色则对应一系列的权限。

对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。

(三)如何利用Spring Security和JWT一起来完成API保护

1.导入依赖

2.配置application.properties

 spring.jackson.serialization.indent_output=true  //JSON格式化
 logging.level.org.springframework.security=info    //打印security日志记录

3.新增 AuthorityName + Authority + 修改 Admins

 

/**
 * 角色枚举类
 */
public enum AuthorityName {
    ROLE_ADMIN,ROLE_USER
}

import java.io.Serializable;

public class Authority implements Serializable {
    private Integer id;
    private AuthorityName name;

    public Authority() {
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public AuthorityName getName() {
        return name;
    }

    public void setName(AuthorityName name) {
        this.name = name;
    }
}
import java.io.Serializable;
import java.util.Date;
import java.util.List;

public class Admins implements Serializable {
    private Integer aid;
    private String  aname;
    private String pwd;
    private Integer aexist;
    private Integer state;
    private Integer doid;
    private String by1;
    private Date lastPasswordResetDate;

    public Date getLastPasswordResetDate() {
        return lastPasswordResetDate;
    }

    public void setLastPasswordResetDate(Date lastPasswordResetDate) {
        this.lastPasswordResetDate = lastPasswordResetDate;
    }

    private List<Authority> authorities;

    public List<Authority> getAuthorities() {
        return authorities;
    }

    public void setAuthorities(List<Authority> authorities) {
        this.authorities = authorities;
    }

    public String getBy1() {
        return by1;
    }

    public void setBy1(String by1) {
        this.by1 = by1;
    }

    public Integer getDoid() {
        return doid;
    }

    public void setDoid(Integer doid) {
        this.doid = doid;
    }

    public Integer getState() {
        return state;
    }

    public void setState(Integer state) {
        this.state = state;
    }

    public Integer getAexist() {
        return aexist;
    }

    public void setAexist(Integer aexist) {
        this.aexist = aexist;
    }

    public Integer getAid() {
        return aid;
    }

    public void setAid(Integer aid) {
        this.aid = aid;
    }

    public String getAname() {
        return aname;
    }

    public void setAname(String aname) {
        this.aname = aname;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }
}

4.创建安全服务用户

JwtUser + JwtUserFactory + JwtUserDetailsServiceImpl + JwtAuthenticationResponse

JwtUSer需要实现UserDetails接口,用户实体即为Spring Security所使用的用户

/**
 * 安全服务的用户
 *  需要实现UserDetails接口,用户实体即为Spring Security所使用的用户
 */
public class JwtUser implements UserDetails {

    private final   Integer id;
    private final  Integer state;
    private final String username;
    private final String password;
    private final String email;
    private final  Collection<? extends GrantedAuthority> authorities;
    private final boolean enabled;
    private final Date lastPasswordResetDate;


    public JwtUser(Integer id, Integer state, String username, String password, String email, Collection<? extends GrantedAuthority> authorities, boolean enabled, Date lastPasswordResetDate) {
        this.id = id;
        this.state = state;
        this.username = username;
        this.password = password;
        this.email = email;
        this.authorities = authorities;
        this.enabled = enabled;
        this.lastPasswordResetDate = lastPasswordResetDate;
    }

    public Integer getState() {
        return state;
    }

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

    @JsonIgnore
    @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;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }

    @JsonIgnore
    public Integer getId() {
        return id;
    }

    public String getEmail() {
        return email;
    }

    @JsonIgnore
    public Date getLastPasswordResetDate() {
        return lastPasswordResetDate;
    }
}
public final  class JwtUserFactory {

    private JwtUserFactory() {
    }

    public static JwtUser create(Admins user){
         return new JwtUser(
                 user.getAid(),
                 user.getState(),
                 user.getAname(),
                 user.getPwd(),
                 user.getEmail(),
                 mapToGrandAuthroties(user.getAuthorities()),
                 user.getAexist()==1?true:false,
                 user.getLastPasswordResetDate()
         );
    }

    private static List<GrantedAuthority> mapToGrandAuthroties(List<Authority> authorities) {
        return authorities.stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getName().name()))
                .collect(Collectors.toList());


    }

}
@Service
public class JwtUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private AdminsMapper adminsMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Admins admins = this.adminsMapper.findByUsername(username);
        if(admins==null){
            throw  new UsernameNotFoundException("No User found with UserName :"+username);
        }else{
          return JwtUserFactory.create(admins);
        }
    }
}
public class JwtAuthenticationResponse implements Serializable {
  private static final long serialVersionUID = 4784951536404964122L;
  private final String token;

  public JwtAuthenticationResponse(String token) {
    this.token = token;
  }

  public String getToken() {
    return this.token;
  }
}

配置 application.properties 支持 mybatis 映射文件 xml

mybatis.mapper-locations=classpath:mybatis/mapper/*.xml

5.创建让Spring控制的安全配置类:WebSecurityConfig

/**
 * 安全配置类
 */
@SuppressWarnings("SpringJavaAutowiringInspection")
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private JwtAuthenticationEntryPoint unauthorizedHandler;

  @Autowired
  private UserDetailsService userDetailsService;


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

  @Autowired
  public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
    authenticationManagerBuilder
            // 设置 UserDetailsService
            .userDetailsService(this.userDetailsService)
            // 使用 BCrypt 进行密码的 hash
            .passwordEncoder(passwordEncoder());
  }

  /**
   * 装载 BCrypt 密码编码器
   *
   * @return
   */
  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

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

  /**
   * token请求授权
   *
   * @param httpSecurity
   * @throws Exception
   */
  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            // we don't need CSRF because our token is invulnerable
            .csrf().disable()
            .cors().and() // 跨域

            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()

            // don't create session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

            .authorizeRequests()
            //.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()

            // allow anonymous resource requests
            .antMatchers(
                    HttpMethod.GET,
                    "/",
                    "/*.html",
                    "/favicon.ico",
                    "/**/*.html",
                    "/**/*.css",
                    "/**/*.js"
            ).permitAll()

            // Un-secure 登录 验证码
            .antMatchers(
                    "/api/auth/**",
                    "/api/verifyCode/**",
                    "/api/global_json"
            ).permitAll()
            // secure other api
            .anyRequest().authenticated();

    // Custom JWT based security filter
    // 将token验证添加在密码验证前面
    httpSecurity
            .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

    // disable page caching
    httpSecurity
            .headers()
            .cacheControl();
  }
}

6.在 XxxController 加一个修饰符 @PreAuthorize("hasRole('ADMIN')") 表示这个资源只能被拥有 ADMIN 角色的用户访问

@RequestMapping(value = "/protectedadmin", method = RequestMethod.GET)
  @PreAuthorize("hasRole('ADMIN')")
  public ResponseEntity<?> getProtectedAdmin() {
    return ResponseEntity.ok("Greetings from admin protected method!");
  }

  @RequestMapping(value = "/protecteduser", method = RequestMethod.GET)
  @PreAuthorize("hasRole('USER')")
  public ResponseEntity<?> getProtectedUser() {
    return ResponseEntity.ok("Greetings from user protected method!");
  }

 

最后,除了 /api/auth, /api/verifycode, /api/global_json 外请求其他的路径
访问抛异常: org.springframework.security.access.AccessDeniedException: Access is denied


集成 JWT 和 Spring Security,完成鉴权登录,获取Token

1.pom.xml中新增依赖 jjwt 依赖

  <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
  <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.0</version>
  </dependency>

  <!-- https://mvnrepository.com/artifact/com.google.code.findbugs/findbugs -->
  <dependency>
      <groupId>com.google.code.findbugs</groupId>
      <artifactId>findbugs</artifactId>
      <version>3.0.1</version>
  </dependency>

2.application.properties 配置 JWT

3.新建一个filter: JwtAuthenticationTokenFilter :用来验证令牌的是否合法

JwtAuthenticationEntryPoint(替代默认弹出登录页面,返回错误信息

+ JwtAuthenticationRequest (登录信息封装类)

+JwtTokenUtil(用于生成令牌,验证等等一些操作)

package com.wutongshu.springboot.security.filter;

import com.wutongshu.springboot.security.JwtTokenUtil;
import io.jsonwebtoken.ExpiredJwtException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

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

/**
 * Jwt 过滤器
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

  private final Log logger = LogFactory.getLog(this.getClass());

  @Autowired
  private UserDetailsService userDetailsService;

    @Value("${jwt.header}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
         final String requestHeader  =  request.getHeader(this.tokenHeader);
         String authToken = null;
         String username = null;
         logger.info(requestHeader);
         //当前请求中包含令牌
         if(requestHeader!=null && requestHeader.startsWith(this.tokenHead)){
             authToken = requestHeader.substring(tokenHead.length());
             try {
                 //根据令牌信息获取用户名
                  username = jwtTokenUtil.getUsernameFromToken(authToken);
             }catch (IllegalArgumentException e){
                 logger.error("an error occured during getting username from the token ",e);
             }catch (ExpiredJwtException e){
                 logger.error("the token is Expried and not invalid anymore",e);
             }

         }else{
             logger.error("couldn't find Beared String,will ignore the request");
         }
         logger.info("checking Authentication with username : " + username);
         //
         if(username!=null && SecurityContextHolder.getContext().getAuthentication()==null){
            UserDetails userDetails =  userDetailsService.loadUserByUsername(username
              );
            if(jwtTokenUtil.validateToken(authToken,userDetails)){
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                logger.info("authorication user: "+username+", setting security context");
                 SecurityContextHolder.getContext().setAuthentication(authentication);
            }
         }
         chain.doFilter(request,response);
    }
}
package com.wutongshu.springboot.security;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;

/**
 * 禁止弹出登录页面,返回错误信息
 */
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // This is invoked when user tries to access a secured REST resource without supplying any credentials
        // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}
package com.wutongshu.springboot.security;

import java.io.Serializable;

public class JwtAuthenticationRequest implements Serializable {

    private String username;
    private String password;

    public JwtAuthenticationRequest() {
    }

    public JwtAuthenticationRequest(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
package com.wutongshu.springboot.security;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;


import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Clock;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

/**
 * 工具类
 *
 */
@Component
public class JwtTokenUtil implements Serializable {
  private static final long serialVersionUID = -3301605591108950415L;
  @SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "It's okay here")
  private Clock clock = DefaultClock.INSTANCE;

  //从application.properties中获取jwt.secret的值,注入到Secret中
  @Value("${jwt.secret}")
  private String secret;

  @Value("${jwt.expiration}")
  private Long expiration;

  //根据token获取username
  public String getUsernameFromToken(String token) {
    return getClaimFromToken(token, Claims::getSubject);
  }

  public Date getIssuedAtDateFromToken(String token) {
    return getClaimFromToken(token, Claims::getIssuedAt);
  }

  public Date getExpirationDateFromToken(String token) {
    return getClaimFromToken(token, Claims::getExpiration);
  }

  public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
    final Claims claims = getAllClaimsFromToken(token);
    return claimsResolver.apply(claims);
  }

  private Claims getAllClaimsFromToken(String token) {
    return Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
  }

  private Boolean isTokenExpired(String token) {
    final Date expiration = getExpirationDateFromToken(token);
    return expiration.before(clock.now());
  }

  private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
    return (lastPasswordReset != null && created.before(lastPasswordReset));
  }

  private Boolean ignoreTokenExpiration(String token) {
    // here you specify tokens, for that the expiration is ignored
    return false;
  }

  public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    return doGenerateToken(claims, userDetails.getUsername());
  }

  private String doGenerateToken(Map<String, Object> claims, String subject) {
    final Date createdDate = clock.now();
    final Date expirationDate = calculateExpirationDate(createdDate);

    return Jwts.builder()
            .setClaims(claims)
            .setSubject(subject)
            .setIssuedAt(createdDate)
            .setExpiration(expirationDate)
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
  }

  public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
    final Date created = getIssuedAtDateFromToken(token);
    return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
            && (!isTokenExpired(token) || ignoreTokenExpiration(token));
  }

  public String refreshToken(String token) {
    final Date createdDate = clock.now();
    final Date expirationDate = calculateExpirationDate(createdDate);

    final Claims claims = getAllClaimsFromToken(token);
    claims.setIssuedAt(createdDate);
    claims.setExpiration(expirationDate);

    return Jwts.builder()
            .setClaims(claims)
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
  }

  public Boolean validateToken(String token, UserDetails userDetails) {
    JwtUser user = (JwtUser) userDetails;
    final String username = getUsernameFromToken(token);
    final Date created = getIssuedAtDateFromToken(token);
    return (
            username.equals(user.getUsername())
                    && !isTokenExpired(token)
                    && !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
    );
  }

  private Date calculateExpirationDate(Date createdDate) {
    return new Date(createdDate.getTime() + expiration * 1000);
  }
}

4.在 WebSecurityConfig 中注入这个filter, 并且配置到 HttpSecurity 中

package com.wutongshu.springboot.security.config;


import com.wutongshu.springboot.security.JwtAuthenticationEntryPoint;
import com.wutongshu.springboot.security.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * 安全配置类
 *
 *
 */
@SuppressWarnings("SpringJavaAutowiringInspection")
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private JwtAuthenticationEntryPoint unauthorizedHandler;

  @Autowired
  private UserDetailsService userDetailsService;


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

  @Autowired
  public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
    authenticationManagerBuilder
            // 设置 UserDetailsService
            .userDetailsService(this.userDetailsService)
            // 使用 BCrypt 进行密码的 hash
            .passwordEncoder(passwordEncoder());
  }

  /**
   * 装载 BCrypt 密码编码器
   *
   * @return
   */
  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

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

  /**
   * token请求授权
   *
   * @param httpSecurity
   * @throws Exception
   */
  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            // we don't need CSRF because our token is invulnerable
            .csrf().disable()
            .cors().and() // 跨域

            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()

            // don't create session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

            .authorizeRequests()
            //.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()

            // allow anonymous resource requests
            .antMatchers(
                    HttpMethod.GET,
                    "/",
                    "/*.html",
                    "/favicon.ico",
                    "/**/*.html",
                    "/**/*.css",
                    "/**/*.js"
            ).permitAll()

            // Un-secure 登录 验证码
            .antMatchers(
                    "/api/auth/**",
                    "/alogin"
            ).permitAll()
            // secure other api
            .anyRequest().authenticated();

    // Custom JWT based security filter
    // 将token验证添加在密码验证前面
    httpSecurity
            .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

    // disable page caching
    httpSecurity
            .headers()
            .cacheControl();
  }
}

最后:

完成鉴权(登录),注册和更新token的功能
.AuthenticationRestController + MethodProtectedRestController + UserRestController

package com.wutongshu.springboot.security.controller;



import com.wutongshu.springboot.security.JwtAuthenticationRequest;
import com.wutongshu.springboot.security.JwtAuthenticationResponse;
import com.wutongshu.springboot.security.JwtTokenUtil;
import com.wutongshu.springboot.security.JwtUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/api")
public class AuthenticationRestController {

  @Value("${jwt.header}")
  private String tokenHeader;

  @Autowired
  private AuthenticationManager authenticationManager;

  @Autowired
  private JwtTokenUtil jwtTokenUtil;

  @Autowired
  private UserDetailsService userDetailsService;

  @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
  public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException {
    UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(
            authenticationRequest.getUsername(),
            authenticationRequest.getPassword()
    );
    // Perform the security
    final Authentication authentication = authenticationManager.authenticate(upToken);
    SecurityContextHolder.getContext().setAuthentication(authentication);

    // Reload password post-security so we can generate token
    final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
    final String token = jwtTokenUtil.generateToken(userDetails);

    // Return the token
    return ResponseEntity.ok(new JwtAuthenticationResponse(token));
  }

  @RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET)
  public ResponseEntity<?> refreshAndGetAuthenticationToken(HttpServletRequest request) {
    String authToken = request.getHeader(tokenHeader);
    final String token = authToken.substring(7);
    String username = jwtTokenUtil.getUsernameFromToken(token);
    JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username);

    if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())) {
      String refreshedToken = jwtTokenUtil.refreshToken(token);
      return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken));
    } else {
      return ResponseEntity.badRequest().body(null);
    }
  }
}
package com.wutongshu.springboot.security.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/api")
public class MethodProtectedRestController {

  /**
   * This is an example of some different kinds of granular restriction for endpoints. You can use the built-in SPEL expressions
   * in @PreAuthorize such as 'hasRole()' to determine if a user has access. Remember that the hasRole expression assumes a
   * 'ROLE_' prefix on all role names. So 'ADMIN' here is actually stored as 'ROLE_ADMIN' in database!
   **/
  @RequestMapping(value = "/protectedadmin", method = RequestMethod.GET)
  @PreAuthorize("hasRole('ADMIN')")
  public ResponseEntity<?> getProtectedAdmin() {
    return ResponseEntity.ok("Greetings from admin protected method!");
  }

  @RequestMapping(value = "/protecteduser", method = RequestMethod.GET)
  @PreAuthorize("hasRole('USER')")
  public ResponseEntity<?> getProtectedUser() {
    return ResponseEntity.ok("Greetings from user protected method!");
  }
}
package com.wutongshu.springboot.security.controller;


import com.wutongshu.springboot.security.JwtTokenUtil;
import com.wutongshu.springboot.security.JwtUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;


@RestController
@RequestMapping("/api")
public class UserRestController {

  @Value("${jwt.header}")
  private String tokenHeader;

  @Autowired
  private JwtTokenUtil jwtTokenUtil;

  @Autowired
  private UserDetailsService userDetailsService;

  /**
   * 获取授权的用户信息
   *
   * @param request
   * @return
   */
  @RequestMapping(value = "/user", method = RequestMethod.GET)
  public JwtUser getAuthenticatedUser(HttpServletRequest request) {
    String token = request.getHeader(tokenHeader).substring(7);
    String username = jwtTokenUtil.getUsernameFromToken(token);
    JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username);
    return user;
  }

}

前台页面:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"></meta>
  <title>JWT Spring Security Demo</title>
  <link rel="shortcut icon" href="favicon.ico" type="image/x-icon"></link>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="../css/bootstrap.min.css"></link>
</head>
<body>
<div class="container">
  <h1>JWT Spring Security Demo</h1>

  <div class="alert alert-danger" id="notLoggedIn">Not logged in!</div>

  <div class="row">
    <div class="col-md-6">
      <div class="panel panel-default" id="login">
        <div class="panel-heading">
          <h3 class="panel-title">Login</h3>
        </div>
        <div class="panel-body">
          <form id="loginForm">
            <div class="form-group">
              <input type="text" class="form-control" id="exampleInputEmail1" placeholder="username"
                     required name="username"/>
            </div>
            <div class="form-group">
              <input type="password" class="form-control" id="exampleInputPassword1"
                     placeholder="password" required name="password">
            </div>
            <div class="well">
              Try one of the following logins
              <ul>
                <li>admin & admin</li>
                <li>user & password</li>
                <li>disabled & password</li>
              </ul>
            </div>
            <button type="submit" class="btn btn-default">login</button>
          </form>
        </div>
      </div>

      <div id="userInfo">
        <div class="panel panel-default">
          <div class="panel-heading">
            <h3 class="panel-title">Authenticated user</h3>
          </div>
          <div class="panel-body">
            <div id="userInfoBody"></div>
            <button type="button" class="btn btn-default" id="logoutButton">logout</button>
          </div>
        </div>
      </div>
    </div>

    <div class="col-md-6">
      <div class="btn-group" role="group" aria-label="..." style="margin-bottom: 16px;">
        <button type="button" class="btn btn-default" id="exampleServiceBtn">call user protected service</button>
        <button type="button" class="btn btn-default" id="adminServiceBtn">call admin protected service</button>
      </div>
      <div class="panel panel-default">
        <div class="panel-heading">
          <h3 class="panel-title">Response:</h3>
        </div>
        <div class="panel-body">
          <pre id="response"></pre>
        </div>
      </div>
    </div>
  </div>

  <div class="row">
    <div id="loggedIn" class="col-md-6">
      <div class="panel panel-default">
        <div class="panel-heading">
          <h3 class="panel-title">Token information</h3>
        </div>
        <div class="panel-body" id="loggedInBody"></div>
      </div>
    </div>
  </div>
</div>

<div class="modal fade" tabindex="-1" role="dialog" id="loginErrorModal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
                aria-hidden="true">&times;</span></button>
        <h4 class="modal-title">Login unsuccessful</h4>
      </div>
      <div class="modal-body"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
      </div>
    </div><!-- /.modal-content -->
  </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

<script src="../js/jquery-3.3.1.min.js"></script>
<!-- Latest compiled and minified JavaScript -->
<script src="../js/bootstrap.min.js"></script>
<script src="../js/jwt-decode.min.js"></script>
<script src="client.js"></script>
</body>
</html>

client.js:

$(function () {
  // VARIABLES =============================================================
  var TOKEN_KEY = "jwtToken"
  var $notLoggedIn = $("#notLoggedIn");
  var $loggedIn = $("#loggedIn").hide();
  var $loggedInBody = $("#loggedInBody");
  var $response = $("#response");
  var $login = $("#login");
  var $userInfo = $("#userInfo").hide();

  // FUNCTIONS =============================================================
  function getJwtToken() {
    return localStorage.getItem(TOKEN_KEY);
  }

  function setJwtToken(token) {
    localStorage.setItem(TOKEN_KEY, token);
  }

  function removeJwtToken() {
    localStorage.removeItem(TOKEN_KEY);
  }

  function doLogin(loginData) {
    $.ajax({
      url: "http://127.0.0.1:8087/test/api/auth",
      type: "POST",
      data: JSON.stringify(loginData),
      contentType: "application/json; charset=utf-8",
      dataType: "json",
      success: function (data, textStatus, jqXHR) {
        setJwtToken(data.token);
        $login.hide();
        $notLoggedIn.hide();
        showTokenInformation();
        showUserInformation();
      },
      error: function (jqXHR, textStatus, errorThrown) {
        if (jqXHR.status === 401) {
          $('#loginErrorModal')
              .modal("show")
              .find(".modal-body")
              .empty()
              .html("<p>Spring exception:<br>" + jqXHR.responseJSON.exception + "</p>");
        } else {
          throw new Error("an unexpected error occured: " + errorThrown);
        }
      }
    });
  }

  function doLogout() {
    removeJwtToken();
    $login.show();
    $userInfo
        .hide()
        .find("#userInfoBody").empty();
    $loggedIn.hide();
    $loggedInBody.empty();
    $notLoggedIn.show();
  }

  function createAuthorizationTokenHeader() {
    var token = getJwtToken();
    if (token) {
      return {"Authorization": "Bearer " + token};
    } else {
      return {};
    }
  }

  function showUserInformation() {
    $.ajax({
      url: "http://127.0.0.1:8087/test/api/user",
      type: "GET",
      contentType: "application/json; charset=utf-8",
      dataType: "json",
      headers: createAuthorizationTokenHeader(),
      success: function (data, textStatus, jqXHR) {
        var $userInfoBody = $userInfo.find("#userInfoBody");

        $userInfoBody.append($("<div>").text("Username: " + data.username));
        $userInfoBody.append($("<div>").text("Email: " + data.email));

        var $authorityList = $("<ul>");
        data.authorities.forEach(function (authorityItem) {
          $authorityList.append($("<li>").text(authorityItem.authority));
        });
        var $authorities = $("<div>").text("Authorities:");
        $authorities.append($authorityList);

        $userInfoBody.append($authorities);
        $userInfo.show();
      }
    });
  }

  function showTokenInformation() {
    var jwtToken = getJwtToken();
    var decodedToken = jwt_decode(jwtToken);

    $loggedInBody.append($("<h4>").text("Token"));
    $loggedInBody.append($("<div>").text(jwtToken).css("word-break", "break-all"));
    $loggedInBody.append($("<h4>").text("Token claims"));

    var $table = $("<table>")
        .addClass("table table-striped");
    appendKeyValue($table, "sub", decodedToken.sub);
    appendKeyValue($table, "aud", decodedToken.aud);
    appendKeyValue($table, "iat", decodedToken.iat);
    appendKeyValue($table, "exp", decodedToken.exp);

    $loggedInBody.append($table);

    $loggedIn.show();
  }

  function appendKeyValue($table, key, value) {
    var $row = $("<tr>")
        .append($("<td>").text(key))
        .append($("<td>").text(value));
    $table.append($row);
  }

  function showResponse(statusCode, message) {
    $response
        .empty()
        .text("status code: " + statusCode + "\n-------------------------\n" + message);
  }

  // REGISTER EVENT LISTENERS =============================================================
  $("#loginForm").submit(function (event) {
    event.preventDefault();

    var $form = $(this);
    var formData = {
      username: $form.find('input[name="username"]').val(),
      password: $form.find('input[name="password"]').val()
    };

    doLogin(formData);
  });

  $("#logoutButton").click(doLogout);

  $("#exampleServiceBtn").click(function () {
    $.ajax({
      url: "http://127.0.0.1:8087/test/api/protecteduser",
      type: "GET",
      contentType: "application/json; charset=utf-8",
      headers: createAuthorizationTokenHeader(),
      success: function (data, textStatus, jqXHR) {
        showResponse(jqXHR.status, data);
      },
      error: function (jqXHR, textStatus, errorThrown) {
        showResponse(jqXHR.status, errorThrown);
      }
    });
  });

  $("#adminServiceBtn").click(function () {
    $.ajax({
      url: "http://127.0.0.1:8087/test/api/protectedadmin",
      type: "GET",
      contentType: "application/json; charset=utf-8",
      headers: createAuthorizationTokenHeader(),
      success: function (data, textStatus, jqXHR) {
        showResponse(jqXHR.status, data);
      },
      error: function (jqXHR, textStatus, errorThrown) {
        showResponse(jqXHR.status, errorThrown);
      }
    });
  });

  $loggedIn.click(function () {
    $loggedIn
        .toggleClass("text-hidden")
        .toggleClass("text-shown");
  });

  // INITIAL CALLS =============================================================
  if (getJwtToken()) {
    $login.hide();
    $notLoggedIn.hide();
    showTokenInformation();
    showUserInformation();
  }
});

效果图:

当我们用admin的用户登录时:

当我们用USER权限的用户登录时

 

 logout方法里清空token

其他注意事项:1.每次请求都必须携带头部信息,而且必须是JSON对象类型,也就是

createAuthorizationTokenHeader()方法

2.WebSecurityConfig配置类里的限制路径并不需要拼接properties里的路径,也就是说方法里实际是controller层的路径

 

posted @ 2019-05-26 21:56  梧桐树master  阅读(9385)  评论(0编辑  收藏  举报