目标
1.Token鉴权
2.Restful API
3.Spring Security+JWT
开始
自行新建Spring Boot工程
引入相关依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>1.5.9.RELEASE</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
User类
非常简单的用户模型,将权限集成到了用户类中。
pacage com.domain
/** * 用户模型 * * @author hackyo * Created on 2017/12/3 11:53. */ public class User { private String id; private String username; private String password; private List<String> roles; ...... 省略get、set方法 ...... }
IUserRepository类
需实现对用户表的增删改查,此处可采用任意数据库,具体实现自行编写。
package com.dao
/** * 用户表操作接口 * * @author hackyo * Created on 2017/12/3 11:53. */ @Component public interface IUserRepository{ /** * 通过用户名查找用户 * * @param username 用户名 * @return 用户信息 */ User findByUsername(String username); }
JwtUser类
安全模块的用户模型
package com.security; import com.fasterxml.jackson.annotation.JsonIgnore; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; /** * 安全用户模型 * * @author hackyo * Created on 2017/12/8 9:20. */ public class JwtUser implements UserDetails { private String username; private String password; private Collection<? extends GrantedAuthority> authorities; JwtUser(String username, String password, Collection<? extends GrantedAuthority> authorities) { this.username = username; this.password = password; this.authorities = authorities; } @Override public String getUsername() { return username; } @JsonIgnore @Override public String getPassword() { return password; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } @JsonIgnore @Override public boolean isEnabled() { return true; } }
JwtTokenUtil类
Token工具类
这里设置了密钥为aaaaaaaa,有效期为2592000秒
package com.security; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.io.Serializable; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * JWT工具类 * * @author hackyo * Created on 2017/12/8 9:20. */ @Component public class JwtTokenUtil implements Serializable { /** * 密钥 */ private final String secret = "aaaaaaaa"; /** * 从数据声明生成令牌 * * @param claims 数据声明 * @return 令牌 */ private String generateToken(Map<String, Object> claims) { Date expirationDate = new Date(System.currentTimeMillis() + 2592000L * 1000); return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact(); } /** * 从令牌中获取数据声明 * * @param token 令牌 * @return 数据声明 */ private Claims getClaimsFromToken(String token) { Claims claims; try { claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } catch (Exception e) { claims = null; } return claims; } /** * 生成令牌 * * @param userDetails 用户 * @return 令牌 */ public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(2); claims.put("sub", userDetails.getUsername()); claims.put("created", new Date()); return generateToken(claims); } /** * 从令牌中获取用户名 * * @param token 令牌 * @return 用户名 */ public String getUsernameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * 判断令牌是否过期 * * @param token 令牌 * @return 是否过期 */ public Boolean isTokenExpired(String token) { try { Claims claims = getClaimsFromToken(token); Date expiration = claims.getExpiration(); return expiration.before(new Date()); } catch (Exception e) { return false; } } /** * 刷新令牌 * * @param token 原令牌 * @return 新令牌 */ public String refreshToken(String token) { String refreshedToken; try { Claims claims = getClaimsFromToken(token); claims.put("created", new Date()); refreshedToken = generateToken(claims); } catch (Exception e) { refreshedToken = null; } return refreshedToken; } /** * 验证令牌 * * @param token 令牌 * @param userDetails 用户 * @return 是否有效 */ public Boolean validateToken(String token, UserDetails userDetails) { JwtUser user = (JwtUser) userDetails; String username = getUsernameFromToken(token); return (username.equals(user.getUsername()) && !isTokenExpired(token)); } }
JwtUserDetailsServiceImpl类
用户验证方法类
package com.security; import com.safepass.dao.IUserRepository; import com.safepass.domain.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.stream.Collectors; /** * 用户验证方法 * * @author hackyo * Created on 2017/12/8 9:18. */ @Service public class JwtUserDetailsServiceImpl implements UserDetailsService { private IUserRepository userRepository; @Autowired public JwtUserDetailsServiceImpl(IUserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username)); } else { return new JwtUser(user.getUsername(), user.getPassword(), user.getRoles().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())); } } }
JwtAuthenticationTokenFilter类
Token过滤器实现
package com.security; import org.springframework.beans.factory.annotation.Autowired; 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; /** * Token过滤器 * * @author hackyo * Created on 2017/12/8 9:28. */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private UserDetailsService userDetailsService; private JwtTokenUtil jwtTokenUtil; @Autowired public JwtAuthenticationTokenFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) { this.userDetailsService = userDetailsService; this.jwtTokenUtil = jwtTokenUtil; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); String tokenHead = "Bearer "; if (authHeader != null && authHeader.startsWith(tokenHead)) { String authToken = authHeader.substring(tokenHead.length()); String username = jwtTokenUtil.getUsernameFromToken(authToken); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }
EntryPointUnauthorizedHandler类
自定义了身份验证失败的返回值
package com.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; /** * 自定401返回值 * * @author hackyo * Created on 2017/12/9 20:10. */ @Component public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { response.setHeader("Access-Control-Allow-Origin", "*"); response.setStatus(401); } }
RestAccessDeniedHandler类
自定了权限不足的返回值
package com.security; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 自定403返回值 * * @author hackyo * Created on 2017/12/9 20:10. */ @Component public class RestAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) { response.setHeader("Access-Control-Allow-Origin", "*"); response.setStatus(403); } }
WebSecurityConfig类
安全配置类
这里设置了禁止访问所有地址,除了用于验证身份的/user/**地址
同时密码的加密方式为BCrypt
package com.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; 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; /** * 安全模块配置 * * @author hackyo * Created on 2017/12/8 9:15. */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private UserDetailsService userDetailsService; private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler; private RestAccessDeniedHandler restAccessDeniedHandler; private PasswordEncoder passwordEncoder; @Autowired public WebSecurityConfig(UserDetailsService userDetailsService, JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter, EntryPointUnauthorizedHandler entryPointUnauthorizedHandler, RestAccessDeniedHandler restAccessDeniedHandler) { this.userDetailsService = userDetailsService; this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter; this.entryPointUnauthorizedHandler = entryPointUnauthorizedHandler; this.restAccessDeniedHandler = restAccessDeniedHandler; this.passwordEncoder = new BCryptPasswordEncoder(); } @Autowired public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and().authorizeRequests() .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() .antMatchers("/user/**").permitAll() .anyRequest().authenticated() .and().headers().cacheControl(); httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); httpSecurity.exceptionHandling().authenticationEntryPoint(entryPointUnauthorizedHandler).accessDeniedHandler(restAccessDeniedHandler); } }
IUserService类
定义用户的基本操作
package com.service; import com.domain.User; /** * 用户操作接口 * * @author hackyo * Created on 2017/12/3 11:53. */ public interface IUserService { /** * 用户登录 * * @param username 用户名 * @param password 密码 * @return 操作结果 */ String login(String username, String password); /** * 用户注册 * * @param user 用户信息 * @return 操作结果 */ String register(User user); /** * 刷新密钥 * * @param oldToken 原密钥 * @return 新密钥 */ String refreshToken(String oldToken); }
UserServiceImpl类
IUserService的实现类,注册时会将用户权限设置为ROLE_USER,同时将密码使用BCrypt加密
package com.service.impl; import com.dao.IUserRepository; import com.domain.User; import com.security.JwtTokenUtil; import com.service.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; /** * 用户操作接口实现 * * @author hackyo * Created on 2017/12/3 11:53. */ @Service public class UserServiceImpl implements IUserService { private AuthenticationManager authenticationManager; private UserDetailsService userDetailsService; private JwtTokenUtil jwtTokenUtil; private IUserRepository userRepository; @Autowired public UserServiceImpl(AuthenticationManager authenticationManager, UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, IUserRepository userRepository) { this.authenticationManager = authenticationManager; this.userDetailsService = userDetailsService; this.jwtTokenUtil = jwtTokenUtil; this.userRepository = userRepository; } @Override public String login(String username, String password) { UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password); Authentication authentication = authenticationManager.authenticate(upToken); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetails userDetails = userDetailsService.loadUserByUsername(username); return jwtTokenUtil.generateToken(userDetails); } @Override public String register(User user) { String username = user.getUsername(); if (userRepository.findByUsername(username) != null) { return "用户已存在"; } BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String rawPassword = user.getPassword(); user.setPassword(encoder.encode(rawPassword)); List<String> roles = new ArrayList<>(); roles.add("ROLE_USER"); user.setRoles(roles); userRepository.insert(user); return "success"; } @Override public String refreshToken(String oldToken) { String token = oldToken.substring("Bearer ".length()); if (!jwtTokenUtil.isTokenExpired(token)) { return jwtTokenUtil.refreshToken(token); } return "error"; } }
UserController类
控制器,控制访问
package com.controller; import com.domain.User; import com.service.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.AuthenticationException; import org.springframework.web.bind.annotation.*; /** * 用户管理Controller * * @author hackyo * Created on 2017/12/3 11:53. */ @CrossOrigin @RestController @RequestMapping(value = "/user", produces = "text/html;charset=UTF-8") public class UserController { private IUserService userService; @Autowired public UserController(IUserService userService) { this.userService = userService; } /** * 用户登录 * * @param username 用户名 * @param password 密码 * @return 操作结果 * @throws AuthenticationException 错误信息 */ @PostMapping(value = "/login", params = {"username", "password"}) public String getToken(String username, String password) throws AuthenticationException { return userService.login(username, password); } /** * 用户注册 * * @param user 用户信息 * @return 操作结果 * @throws AuthenticationException 错误信息 */ @PostMapping(value = "/register") public String register(User user) throws AuthenticationException { return userService.register(user); } /** * 刷新密钥 * * @param authorization 原密钥 * @return 新密钥 * @throws AuthenticationException 错误信息 */ @GetMapping(value = "/refreshToken") public String refreshToken(@RequestHeader String authorization) throws AuthenticationException { return userService.refreshToken(authorization); } }
使用
只需要在方法或类上加注解即可实现账号控制
例如,我们想控制该方法只允许用户本人使用,#号表示方法的参数,可以在参数中加上@P('name')来指定名称,同时也可直接使用模型,如user.username等
总之,其中可以写入任何Spring EL
@PreAuthorize("#username == authentication.name") @GetMapping(value = "/getInfo") public String getInfo(String username) { return JSON.toJSONString(userService.getInfo(username)); }
另外也可以自定义控制注解,使用@PostFilter注解,并实现hasPermission类即可,同时需要在WebSecurityConfigurerAdapter中开启。
测试
运行程序后,我们使用Postman进行测试
1.注册
URL:http://localhost:8080/user/register
参数:username、password
返回success即为成功
2.登录
URL:http://localhost:8080/user/login
参数:username、password
可以看到服务器将我们的Token返回了
3.刷新Token
URL(GET方法):http://localhost:8080/user/refreshToken
参数:在Header中加入登录时返回的Token,注意,需要在Token前加上“Bearer ”,最后有个空格
Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE1MTMzMTE1NjMsInN1YiI6IjEyMyIsImNyZWF0ZWQiOjE1MTI3MDY3NjM3NjB9.baiY8QcbJgq4FQMC2piN1smbW57WjDDTiRVIL9hJeC_DcPgcyJweWqkS6g7825mPKFlByuUx7XN8nUOIszDVcw
可以看到服务器给我们返回了新的Token,如果我们不加上Token的话,将无法访问