引:前段时间写了个前后台不分离的SSMLogin,大家可能会觉得前后台不分离太low,所以这次我们使用SpringBoot+Angular搭建了一个利用Token来进行身份验证的前后台分离登录Demo项目。
项目源码
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
29export 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'})
};
()
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 ()
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 ()
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
(produces = MediaType.APPLICATION_JSON_VALUE)
public class AuthenticationController {
private AuthenticationManager authenticationManager;
private JwtTokenUtil jwtTokenUtil;
private UserDetailsService userDetailsService;
// 请求路径(/auth)
"${jwt.authenticationPath}") (value =
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
public class UserDetailsServiceImpl implements UserDetailsService{
private UserRepository userRepository;
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
单元测试
我们可以先定义总的接口:1
2
3
4 (SpringRunner.class)
public class SpringBootTestAbstract {
}
然后每个要测试的类继承他就可以了,避免了重复配置,IDEA还有很多技巧,自己可以去baidu、google,这里也举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class DataSourceTest extends SpringBootTestAbstract{
ApplicationContext applicationContext;
DataSourceProperties dataSourceProperties;
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