HelloWorld_SpringBoot Angular

引:前段时间写了个前后台不分离的SSMLogin,大家可能会觉得前后台不分离太low,所以这次我们使用SpringBoot+Angular搭建了一个利用Token来进行身份验证的前后台分离登录Demo项目。

项目源码

SpringBoot+Angular

Angular前端

本文前端基于Angular官方样例Tour of Heroes,请先到官网下载,也可以拷贝自己的项目源码(不过最好还是一步步来,先去官网下载源码),这次前端不是主要讲解的地方,所以官网源码部分就不说了,自己去看。我只说增加的核心部分。

login组件

下面是登录的ts代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export class LoginComponent implements OnInit {
// 用来接收后台的数据
model: any = {};

//注入依赖对象
constructor(
private router: Router,
private authenticationService: AuthenticationService,
) { }

ngOnInit() {
// 重置登录状态
this.authenticationService.logout();
}

login() {
// 利用authenticationService获取对象
this.authenticationService.login(this.model.username, this.model.password).subscribe(
result => {
if (result) {
// 登录成功,调制路由
this.router.navigate(['dashboard']);
} else {
this.log('Username or Password is incorrect');
}
}
)
}
}

AuthenticationService服务

看看它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// http头,使用json传递数据
const httpOptions = {
headers: new HttpHeaders({'Content-Type': 'application/json'})
};


@Injectable()
export class AuthenticationService {
// 认证的url
private url: string = `${environment.apiURL}/auth`;

constructor(private http: HttpClient) {
}

login(username: string, password: string): Observable<boolean> {
return this.http.post<any>(this.url,JSON.stringify({username: username, password: password}),httpOptions).pipe(
tap(response => {
// 获得token认证
let token = response.token;
if (token) {
// 存储token到浏览器localStorage中,以后每次都带着它去请求数据
localStorage.setItem('currentUser',token);
}
}),
catchError(err => {
console.error(err);
return of (false);
}

)
)
}

// 从localStorage获得token
getToken(): String {
return localStorage.getItem('currentUser');
}

logout(): void {
// 清空token,那么没有Token也就不能刷新页面了
localStorage.removeItem('currentUser');
}

isLoggedIn(): boolean {
var token: String = this.getToken();
return token && token.length > 0;
}

}

AuthenticationInterceptor拦截器

有了这个拦截器,以后所有的请求都过经过这里,看看它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Injectable()
export class AuthenticationInterceptor implements HttpInterceptor {

// next 相当于Java Filter 的chain
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const idToken = localStorage.getItem('currentUser');

if (idToken) {
// 将原来的请求加上token包装成新的请求发送
const cloned = req.clone({
headers: req.headers.set('Authorization', 'Bearer ' + idToken)
});

return next.handle(cloned);
} else {
return next.handle(req);
}
}
}

CanActivateAuthGuard保卫

有了CanActivateAuthGuard就能够防止未登录用户访问其他页面,看看它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Injectable()
export class CanActivateAuthGuard implements CanActivate {

constructor(
public router: Router,
private authService: AuthenticationService
) {}

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (this.authService.isLoggedIn()) {
// 可以通过
return true;
}

// 不能通过,跳转到登录页面
this.router.navigate(['/login']);
return false;
}
}

Springboot后台

我们首先要对SpringBoot的项目结构有个熟悉的认识,如果不知道的可以先稍微了解一下他,他太棒了。这次开发也是上帝模式。

SpringSecurity权限控制

首先我们要对前台的请求进行认证,利用配置文件设置一些值:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 控制跨域访问
cors:
allowedOrigins: "*"
allowedMethods: GET,POST,DELETE,PUT,OPTIONS
allowedHeaders: Origin,X-Requested-With,Content-Type,Accept,Accept-Encoding,Accept-Language,Host,Referer,Connection,User-Agent,Authorization

# token配置
jwt:
header: Authorization
secret: mySecret
expiration: 7200
issuer: IATA
authenticationPath: /auth

SpringSecurity配置类

在com.todorex.config.WebSecurityConfig类中,在这个类中,我们主要是配置认证路径、跨域访问以及过滤器等设置。

SpringSecurity自定义过滤器

在com.todorex.config.AuthenticationTokenFilter类中,主要是验证token,但不重新计算token。

token生成方法

在com.todorex.util.JwtTokenUtil类中,主要有生成token以及验证token的方法。

Contoller

主要是控制处理URL,其中比较重要的就是com.todorex.controller.AuthenticationController类,它是用来进行权限认证的,我们看看它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@RestController
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Autowired
private UserDetailsService userDetailsService;

// 请求路径(/auth)
@PostMapping(value = "${jwt.authenticationPath}")
public AuthenticationResponse login(@RequestBody AuthenticationRequest request) throws AuthenticationException {
// 对包装过后的UsernamePasswordAuthenticationToken精心再包装成Authentication
final Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
// 将该认证添加到上下文中(避免多次验证)
// SecurityContextHolder用于存储安全上下文(security context)的信息
SecurityContextHolder.getContext().setAuthentication(authentication);


final UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
// 实际应用中,生成token时可能会用到更多的参数
final String token = jwtTokenUtil.generate(userDetails.getUsername());
// 返回Token
return new AuthenticationResponse(token);

}
}

其他都是普通的Controller,但是对于URL的设计我们需要注意,我们主要是用Restful风格的URL,这种风格是怎么样的呢?我们可以参考下面的博文:【Restful】三分钟彻底了解Restful最佳实践

Service

主要是处理业务的,这里我们需要注意的这个类:com.todorex.service.UserDetailsServiceImpl,我们需要实现UserDetailsService接口,因为SpringSecurity方法会用到,看看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Service
public class UserDetailsServiceImpl implements UserDetailsService{

@Autowired
private UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userRepository.findByUsername(s);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", s));
} else {
return create(user);
}

}

/**
* 包装成SpringSecurity需要的User
* @param user
* @return
*/
private static org.springframework.security.core.userdetails.User create(User user) {
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), mapToGrantedAuthorities(user.getAuthorities()));
}

/**
* 包装权限
* @param authorities
* @return
*/
private static List<GrantedAuthority> mapToGrantedAuthorities(List<Authority> authorities) {
// Java8语法
return authorities.stream()
.map(authority -> new SimpleGrantedAuthority(authority.getName().name()))
.collect(Collectors.toList());
}

}

Dao

这里我们使用的JPA,它内部实现是Hibernate,我们只要先定义Entity再定义Dao类就可以了。

Entity

Entity定义之后可以根据实体自动建表(可在配置文件中配置),如下:

1
2
3
4
5
6
7
# jpa配置
jpa:
database: MYSQL
show-sql: true
hibernate:
# 表不存在就自动建表
ddl-auto: create

Dao

Dao接口只要继承JpaRepository就会有很多内置方法,如果要自己写SQL也可以参照com.todorex.HeroRepository的写法。

单元测试

我们可以先定义总的接口:

1
2
3
4
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootTestAbstract {
}

然后每个要测试的类继承他就可以了,避免了重复配置,IDEA还有很多技巧,自己可以去baidu、google,这里也举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DataSourceTest extends SpringBootTestAbstract{
@Autowired
ApplicationContext applicationContext;
@Autowired
DataSourceProperties dataSourceProperties;

@Test
public void testDataSource() throws Exception {
// 获取配置的数据源
DataSource dataSource = applicationContext.getBean(DataSource.class);
// 查看配置数据源信息
System.out.println(dataSource);
System.out.println(dataSource.getClass().getName());
System.out.println(dataSourceProperties);
}
}

日志

SpringBoot默认使用logback作为日志框架,我们可以做如下配置:

1
2
3
4
5
6
7
8
9
# 日志配置
logging:
level:
# 配置包以及输出等级
com.todorex: debug
pattern:
console: "%d - %msg%n"
path: /var/localLog/
file: /var/localLog/springbootdemo.log

参考

  1. Angular 5集成Spring Boot,Spring Security,JWT和CORS
  2. Angular_Heroes教程
  3. 揭秘SpringSecurity(一)_请求认证过程
  4. 揭秘SpringBoot(一)_SpringBoot运行原理
  5. 【Restful】三分钟彻底了解Restful最佳实践
  6. 什么是 JWT – JSON WEB TOKEN
  7. ajax 跨域 CROS
  8. Spring Boot 日志配置(超详细)