Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。
Spring Security 相对于Shiro权限框架而已要稍微复杂一点.
spring Security官网
JWT 英文名是 Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。
JWT 以 JSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。
JWT官网
io.jsonwebtoken
jjwt
0.9.1
org.springframework.boot
spring-boot-starter-security
@SuppressWarnings("SpringJavaAutowiringInspection")
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthExcetion.MyAuthenticationEntry myAuthenticationEntry;
@Autowired
private MyAuthExcetion.MyAccessDenied myAccessDenied;
@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(userDetailsService)
// 使用 BCrypt 进行密码的 hash
.passwordEncoder(passwordEncoder());
}
/**
* 装载BCrypt密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置自定义jwt权限认证过滤器
*/
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
/**
* 设置需要授权认证的资源,不需要权认能访问的资源
*/
@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(myAuthenticationEntry).and()
.exceptionHandling().accessDeniedHandler(myAccessDenied).and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
//过滤掉不需要权限的地址
.antMatchers(HttpMethod.GET, "/").permitAll()
.antMatchers("/sso/login").permitAll()
// 其它接口都要认证
.anyRequest().authenticated();
//将token验证添加在密码验证前面
httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity.headers().cacheControl();
}
}
token参数配置
在application.yml中配置token参数信息
server:
port: 8016
jwt:
# 加密密钥
secret: iwqjhda8232bjgh432[cicada-smile]
#token有效时长 (单位秒)
expire: 3600
#请求头参数名称
tokenHead: token
生成token工具类
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtTokenUtils {
private String secret;
private long expire;
private String tokenHead;
//..省略get set方法
//jdk8新增的时钟类
private Clock clock = DefaultClock.INSTANCE;
/**
* 使用用户名作为身份信息生成Token
* @param identityId 用户身份标识
* @param identityId 用户的角色权限信息
*/
public Map getToken(String identityId, List<String> Authorizes) {
Date nowDate = new Date();
//token过期时间
long expireAt=nowDate.getTime() + expire * 1000;
Date expireDate = new Date(expireAt);
Map map=new HashMap<>();
map.put("expireAt",expireAt);
String token= Jwts.builder()
//放入唯一标识,可以是用户名或者Id
.setSubject(identityId)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
//自定义属性 放入用户拥有角色和权限
.claim("roleAuthorizes", Authorizes)
.compact();
map.put("token",token);
return map;
}
/**
* 根据token获取身份信息
*/
public Claims getTokenClaim(String token) {
try {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 判断token是否失效
*/
public boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(clock.now());
}
/**
* 根据token获取username
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* 根据token获取失效时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getTokenClaim(token);
return claimsResolver.apply(claims);
}
}
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtils jwtTokenUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String token = request.getHeader(jwtTokenUtils.getTokenHead());
//判断当前请求中包含令牌
if (!StringUtils.isEmpty(token)) {
//重token中获取用户的角色权限信息
Claims claims = jwtTokenUtils.getTokenClaim(token);
if (claims != null) {
//如果token未失效 并且 当前上下文权限凭证为null
if (!jwtTokenUtils.isTokenExpired(token) && SecurityContextHolder.getContext().getAuthentication() == null) {
/**
* 这里省略了查询数据库token存不存在,有没有失效这个步骤
* 如果是做单点登录,后登录的人登录时候把上一个人的token状态改为失效,这样就能保证同一时间一个帐号只能有一个人能使用
*/
JwtUserDetail userDetails = new JwtUserDetail(claims);
//把用户权限信息放到上下文中
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
}
/**
* @author Dominick Li
* @CreateTime 2020/4/16 21:28
* @description 重token中获取用户权限信息
**/
public class JwtUserDetail implements UserDetails {
private String username;
private Collection<SimpleGrantedAuthority> authorities;
public JwtUserDetail(Claims claims) {
this.username = claims.getSubject();
//重token中获取权限信息
List<String> roleAuthorizes = claims.get("roleAuthorizes", List.class);
//权限和角色都在roleAuthorizes中,和shiro不同,shiro是权限和角色分开的
authorities = new ArrayList<>(roleAuthorizes.size());
roleAuthorizes.forEach((roleAuthorize) -> {
authorities.add(new SimpleGrantedAuthority(roleAuthorize));
});
}
@Override
public Collection<SimpleGrantedAuthority> getAuthorities() {
return authorities;
}
@Override
@JsonIgnore
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
public class MyAuthExcetion {
@Component
public static class MyAuthenticationEntry implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
//token认证失败
HashMap map = new HashMap();
map.put("code", 402);
map.put("msg", "toekn is invalid");
response.getWriter().println(map.toString());
}
}
@Component
public static class MyAccessDenied implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
//角色权限认证失败
HashMap map = new HashMap();
map.put("code", 401);
map.put("msg", "accessDenied");
response.getWriter().println(map.toString());
}
}
}
为方便测试,用户角色权限信息写死了
需要注意的事项,在把角色放入到资源列表中需要在角色前面添加ROLE_
@Service
public class UserDetailsFactory implements UserDetailsService {
/**
* 模拟数据库
* 存放用户对应的角色
*/
HashMap<String, String> userRole = new HashMap<>();
{
userRole.put("test", "USER");
userRole.put("admin", "ADMIN");
}
/**
* 模拟数据库
* 存放角色对应的权限
*/
HashMap<String, List<GrantedAuthority>> roleAuthorize = new HashMap<>();
{
List<GrantedAuthority> uList = new ArrayList<>();
uList.add(new SimpleGrantedAuthority("select"));
uList.add(new SimpleGrantedAuthority("put"));
roleAuthorize.put("USER", uList);
List<GrantedAuthority> aList =new ArrayList<>();
aList.add(new SimpleGrantedAuthority("select"));
aList.add(new SimpleGrantedAuthority("delete"));
roleAuthorize.put("ADMIN", aList);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//加密过的密码 123456
String password="$2a$10$JA61ycV.otx/d8cJSPAi8Ozbvij5QwDTFxp7jq9Qq3pm0xuZbW6ji";
String roleName=userRole.get(username);
List<GrantedAuthority> authorityList=roleAuthorize.get(roleName);
//security资源注解是把角色和权限一起处理的,shiro是分开处理,需要手动在角色前面添加ROLE_
roleName="ROLE_"+roleName;
authorityList.add(new SimpleGrantedAuthority(roleName));
return User.builder().username(username).password(password).authorities(authorityList).build();
}
}
@Component
public class UserService {
@Autowired
private JwtTokenUtils jwtTokenUtils;
@Autowired
UserDetailsFactory userDetailService;
@Autowired
PasswordEncoder passwordEncoder;
public Map login(String username, String password) {
Map map = null;
UserDetails user = userDetailService.loadUserByUsername(username);
if (!passwordEncoder.matches(/*原始密码*/password,/*加密过的密码*/user.getPassword())) {
map = new HashMap();
map.put("code", 303);
map.put("msg", "密码错误");
}
List<String> authorites = new ArrayList<>(user.getAuthorities().size());
for (GrantedAuthority authorite : user.getAuthorities()) {
authorites.add(authorite.getAuthority());
}
map = jwtTokenUtils.getToken(username, authorites);
map.put("code",200);
return map;
}
}
@RestController
public class TokenController {
@Autowired
private UserService userService;
/**
* 登录接口
*/
@PostMapping("/sso/login")
public Map login(@RequestParam("username") String username,
@RequestParam("password") String password) {
// 省略数据源校验
return userService.login(username,password);
}
/**
* 需要角色user才能访问的地址
*/
@PreAuthorize("hasRole('USER')")
@GetMapping("/index")
public String info() {
return "index";
}
/**
* 需要角色admin才能访问的地址
*/
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String admin() {
return "admin";
}
/**
* 需要权限delete 才能删除用户信息
*/
@DeleteMapping("/user/{id}")
@PreAuthorize("hasAuthority('delete')")
public String deleteUser(@PathVariable Integer id) {
System.out.println("删除用户:id=" + id);
return "user";
}
/**
* 需要权限select 才能查看用户信息
*/
@GetMapping("/user/{id}")
@PreAuthorize("hasAuthority('select')")
public String user(@PathVariable Integer id) {
System.out.println("查询用户:id=" + id);
return "user";
}
/**
* 需要权限put 才能修改用户信息
*/
@PutMapping("/user")
@PreAuthorize("hasAuthority('put')")
public String putUser() {
System.out.println("修改用户:id");
return "user";
}
}
github地址
创作不易,要是觉得我写的对你有点帮助的话,麻烦在github上帮我点下 Star
【SpringBoot框架篇】其它文章如下,后续会继续更新。