目标
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类
安全模块的用户模型
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秒
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;
/**
}
JwtUserDetailsServiceImpl类
用户验证方法类
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过滤器实现
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类
自定义了身份验证失败的返回值
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类
自定了权限不足的返回值
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
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类
定义用户的基本操作
import com.domain.User;
/**
}
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类
控制器,控制访问
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的话,将无法访问
参考:
http://www.jianshu.com/p/6307c89fe3fa
http://www.jianshu.com/p/4468a2fff879