关于 Spring Security Web系统的认证和权限模块也算是一个系统的基础设施了,几乎任何的互联网服务都会涉及到这方面的要求。在Java EE领域,成熟的安全框架解决方案通常有 Apache Shiro、Spring Security等两种技术选型。Apache Shiro简单易用也算是一大优点,但其功能仍是远不如 Spring Security强大。Spring Security能够为 Spring 应用提供声明式的安全访问控制,起经过提供一系列能够在 Spring应用上下文中可配置的Bean,并利用 Spring IoC和 AOP等功能特性来为应用系统提供声明式的安全访问控制功能,减小了诸多重复工做。java
关于JWT JSON Web Token (JWT),是在网络应用间传递信息的一种基于 JSON的开放标准((RFC 7519),用于做为JSON对象在不一样系统之间进行安全地信息传输。主要使用场景通常是用来在 身份提供者和服务提供者间传递被认证的用户身份信息。关于JWT的科普,能够看看阮一峰老师的《JSON Web Token 入门教程》。mysql
本文则结合 Spring Security和 JWT两大利器来打造一个简易的权限系统。git
本文实验环境以下:github
2.0.6.RELEASE
IntelliJ IDEA 2018.2.4
另外本文实验代码置于文尾,须要自取。web
可 长按 或 扫描 下面的 当心心 来订阅做者公众号 CodeSheep,获取更多 务实、能看懂、可复现的 原创文 ↓↓↓spring
本文实验为了简化考虑,准备作以下设计:sql
role
,包括角色ID
和角色名称
user
,包括用户ID
,用户名
,密码
user_roles
一个用户能够拥有多个角色
pom.xml
中引入 Spring Security和 JWT所必需的依赖<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
复制代码
server.port=9991
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://121.196.XXX.XXX:3306/spring_security_jwt?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=XXXXXX
logging.level.org.springframework.security=info
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jackson.serialization.indent_output=true
复制代码
用户实体 User:数据库
/** * @ www.codesheep.cn * 20190312 */
@Entity
public class User implements UserDetails {
@Id
@GeneratedValue
private Long id;
private String username;
private String password;
@ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
private List<Role> roles;
...
// 下面为实现UserDetails而须要的重写方法!
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add( new SimpleGrantedAuthority( role.getName() ) );
}
return authorities;
}
...
}
复制代码
此处所建立的 User类继承了 Spring Security的 UserDetails接口,从而成为了一个符合 Security安全的用户,即经过继承 UserDetails,便可实现 Security中相关的安全功能。json
角色实体 Role:安全
/** * @ www.codesheep.cn * 20190312 */
@Entity
public class Role {
@Id
@GeneratedValue
private Long id;
private String name;
... // 省略 getter和 setter
}
复制代码
主要用于对 JWT Token进行各项操做,好比生成Token、验证Token、刷新Token等
/** * @ www.codesheep.cn * 20190312 */
@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -5625635588908941275L;
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
public String generateToken(UserDetails userDetails) {
...
}
String generateToken(Map<String, Object> claims) {
...
}
public String refreshToken(String token) {
...
}
public Boolean validateToken(String token, UserDetails userDetails) {
...
}
... // 省略部分工具函数
}
复制代码
/** * @ www.codesheep.cn * 20190312 */
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal ( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader( Const.HEADER_STRING );
if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) {
final String authToken = authHeader.substring( Const.TOKEN_PREFIX.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);
}
}
复制代码
主要包括用户登陆和注册两个主要的业务
public interface AuthService {
User register( User userToAdd );
String login( String username, String password );
}
复制代码
/** * @ www.codesheep.cn * 20190312 */
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserRepository userRepository;
// 登陆
@Override
public String login( String username, String password ) {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password );
final Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
final UserDetails userDetails = userDetailsService.loadUserByUsername( username );
final String token = jwtTokenUtil.generateToken(userDetails);
return token;
}
// 注册
@Override
public User register( User userToAdd ) {
final String username = userToAdd.getUsername();
if( userRepository.findByUsername(username)!=null ) {
return null;
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
final String rawPassword = userToAdd.getPassword();
userToAdd.setPassword( encoder.encode(rawPassword) );
return userRepository.save(userToAdd);
}
}
复制代码
这是一个高度综合的配置类,主要是经过重写 WebSecurityConfigurerAdapter
的部分 configure
配置,来实现用户自定义的部分。
/** * @ www.codesheep.cn * 20190312 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Bean
public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtTokenFilter();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure( AuthenticationManagerBuilder auth ) throws Exception {
auth.userDetailsService( userService ).passwordEncoder( new BCryptPasswordEncoder() );
}
@Override
protected void configure( HttpSecurity httpSecurity ) throws Exception {
httpSecurity.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS请求所有放行
.antMatchers(HttpMethod.POST, "/authentication/**").permitAll() //登陆和注册的接口放行,其余接口所有接受验证
.antMatchers(HttpMethod.POST).authenticated()
.antMatchers(HttpMethod.PUT).authenticated()
.antMatchers(HttpMethod.DELETE).authenticated()
.antMatchers(HttpMethod.GET).authenticated();
// 使用前文自定义的 Token过滤器
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
httpSecurity.headers().cacheControl();
}
}
复制代码
登陆和注册的 Controller:
/**
* @ www.codesheep.cn
* 20190312
*/
@RestController
public class JwtAuthController {
@Autowired
private AuthService authService;
// 登陆
@RequestMapping(value = "/authentication/login", method = RequestMethod.POST)
public String createToken( String username,String password ) throws AuthenticationException {
return authService.login( username, password ); // 登陆成功会返回JWT Token给用户
}
// 注册
@RequestMapping(value = "/authentication/register", method = RequestMethod.POST)
public User register( @RequestBody User addedUser ) throws AuthenticationException {
return authService.register(addedUser);
}
}
复制代码
再编写一个测试权限的 Controller:
/**
* @ www.codesheep.cn
* 20190312
*/
@RestController
public class TestController {
// 测试普通权限
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
@RequestMapping( value="/normal/test", method = RequestMethod.GET )
public String test1() {
return "ROLE_NORMAL /normal/test接口调用成功!";
}
// 测试管理员权限
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@RequestMapping( value = "/admin/test", method = RequestMethod.GET )
public String test2() {
return "ROLE_ADMIN /admin/test接口调用成功!";
}
}
复制代码
这里给出两个测试接口用于测试权限相关问题,其中接口 /normal/test
须要用户具有普通角色(ROLE_NORMAL
)便可访问,而接口/admin/test
则须要用户具有管理员角色(ROLE_ADMIN
)才能够访问。
接下来启动工程,实验测试看看效果
在文章开头咱们即在用户表 user
中插入了一条用户名为 codesheep
的记录,并在用户-角色表 user_roles
中给用户 codesheep
分配了普通角色(ROLE_NORMAL
)和管理员角色(ROLE_ADMIN
)
接下来进行用户登陆,并得到后台向用户颁发的JWT Token
不带 Token直接访问须要普通角色(ROLE_NORMAL
)的接口 /normal/test
会直接提示访问不通:
而带 Token访问须要普通角色(ROLE_NORMAL
)的接口 /normal/test
才会调用成功:
同理因为目前用户具有管理员角色,所以访问须要管理员角色(ROLE_ADMIN
)的接口 /admin/test
也能成功:
接下里咱们从用户-角色表里将用户codesheep
的管理员权限删除掉,再访问接口 /admin/test
,会发现因为没有权限,访问被拒绝了:
通过一系列的实验过程,也达到了咱们的预期!
本文涉及的东西仍是蛮多的,最后咱们也将本文的实验源码放在 Github上,须要的能够自取:源码下载地址
因为能力有限,如有错误或者不当之处,还请你们批评指正,一块儿学习交流!