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">×</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层的路径